aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@toptal.com>2026-04-25 23:06:17 +0100
committerBozhidar Batsov <bozhidar@toptal.com>2026-04-25 23:06:17 +0100
commit6be2d06216a74b09b96315c6878fad8f3923d7e1 (patch)
tree2c54b6fbebf2a696b810660c1d9f66eeced5d0ec
parent2b49c82126c12180bbabbd929698dc35192b1a75 (diff)
Soft-deprecate prefix-less dirconfig entriesdirconfig-refactor
The implicit "any unprefixed line is an ignore pattern" rule is the last source of subtle parser surprises — it's the reason why a single leading space silently changes a +-keep into a literal ignore pattern, and it makes typo'd comments slip through as ignores. Mark these lines as :legacy-ignore in the classifier, record them in a new prefixless-ignore slot on the dirconfig struct, and emit a one-time warning per project listing the offending entries. The behavior is unchanged — the lines still go into the ignore list — but users now get a nudge to write them as -entry. The warning can be silenced via projectile-warn-on-prefixless-dirconfig-lines.
-rw-r--r--CHANGELOG.md3
-rw-r--r--doc/modules/ROOT/pages/projects.adoc9
-rw-r--r--projectile.el97
-rw-r--r--test/projectile-test.el65
4 files changed, 140 insertions, 34 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4e33a2..24b0cd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@
### 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'.
+* `projectile-parse-dirconfig-file' now returns a `projectile-dirconfig' struct (with `keep', `ignore', `ensure', and `prefixless-ignore' slots) instead of a positional 3-tuple. External callers should use the accessors (`projectile-dirconfig-keep' etc.) rather than `car'/`cadr'/`caddr'.
+* Soft-deprecate prefix-less ignore entries in `.projectile'. Lines without a `+'/`-'/`!' prefix are still treated as ignore patterns for backward compatibility, but a one-time warning is now shown for each project that uses them. Set `projectile-warn-on-prefixless-dirconfig-lines' to nil to silence.
### New features
diff --git a/doc/modules/ROOT/pages/projects.adoc b/doc/modules/ROOT/pages/projects.adoc
index f1f4a5f..24e6e31 100644
--- a/doc/modules/ROOT/pages/projects.adoc
+++ b/doc/modules/ROOT/pages/projects.adoc
@@ -813,8 +813,13 @@ If you'd like to instruct Projectile to ignore certain files in a
project, when indexing it you can do so in the `.projectile` file by
adding each path to ignore, where the paths all are relative to the
root directory and start with a slash. Everything ignored should be
-preceded with a `-` sign. Alternatively, not having any prefix at all
-also means to ignore the directory or file pattern that follows.
+preceded with a `-` sign.
+
+NOTE: Lines without any prefix at all are still accepted and treated
+as ignore patterns for backward compatibility, but the implicit form
+is being phased out and Projectile now warns about it once per
+project. Prefer the explicit `-` prefix in new dirconfigs.
+
Here's an example for a typical Rails application:
----
diff --git a/projectile.el b/projectile.el
index 7217397..f600597 100644
--- a/projectile.el
+++ b/projectile.el
@@ -415,6 +415,17 @@ silent bypass is a frequent source of confusion."
:type 'boolean
:package-version '(projectile . "2.10.0"))
+(defcustom projectile-warn-on-prefixless-dirconfig-lines t
+ "Whether to warn about deprecated prefix-less ignore entries.
+Lines in `.projectile' that start with no `+'/`-'/`!' prefix are
+still accepted as ignore patterns for backward compatibility, but
+the implicit form is being phased out. When this option is
+non-nil, a one-time warning is shown per project that uses any
+such line, listing the offending entries."
+ :group 'projectile
+ :type 'boolean
+ :package-version '(projectile . "2.10.0"))
+
(defcustom projectile-globally-ignored-files
(list projectile-tags-file-name projectile-cache-file)
"A list of files globally ignored by projectile.
@@ -667,6 +678,9 @@ Each value is a cons of (MTIME . PARSED-RESULT).")
(defvar projectile--alien-dirconfig-warned-projects (make-hash-table :test 'equal)
"Set of project roots already warned about alien indexing skipping the dirconfig.")
+(defvar projectile--prefixless-dirconfig-warned-projects (make-hash-table :test 'equal)
+ "Set of project roots already warned about prefix-less dirconfig entries.")
+
(defvar projectile-known-projects nil
"List of locations where we have previously seen projects.
The list of projects is ordered by the time they have been accessed.
@@ -2193,9 +2207,11 @@ Unignored files/directories are not included."
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))
+respectively. PREFIXLESS-IGNORE is the subset of IGNORE entries
+that arrived without a leading `+'/`-'/`!'/comment marker; they
+are accepted for backward compatibility but recorded separately so
+callers can flag the deprecated syntax. All slots default to nil."
+ (keep nil) (ignore nil) (ensure nil) (prefixless-ignore nil))
(defun projectile--warn-glob-in-keep-entry (entry dirconfig)
"Warn that ENTRY in DIRCONFIG looks like a glob pattern after a `+'.
@@ -2212,9 +2228,12 @@ or move the pattern to a `-'/`!' rule."
(defun projectile--dirconfig-classify-line (line)
"Classify LINE from a dirconfig file.
Return a cons (BUCKET . VALUE) where BUCKET is one of `:keep',
-`:ignore', `:ensure', or `:comment'. Return nil for a blank line.
-Leading whitespace is skipped before dispatch so an accidental space
-or tab before the prefix does not change classification."
+`:ignore', `:ensure', `:legacy-ignore', or `:comment'. Return nil
+for a blank line. Leading whitespace is skipped before dispatch
+so an accidental space or tab before the prefix does not change
+classification. `:legacy-ignore' is reserved for prefix-less
+lines, which are still treated as ignore patterns for backward
+compatibility but are tracked separately so callers can warn."
(let* ((trimmed (string-trim-left line))
(first-char (and (> (length trimmed) 0) (aref trimmed 0))))
(cond
@@ -2222,23 +2241,27 @@ or tab before the prefix does not change classification."
((and projectile-dirconfig-comment-prefix
(eql first-char projectile-dirconfig-comment-prefix))
(cons :comment nil))
- ((eql first-char ?+) (cons :keep (string-trim (substring trimmed 1))))
- ((eql first-char ?-) (cons :ignore (string-trim (substring trimmed 1))))
- ((eql first-char ?!) (cons :ensure (string-trim (substring trimmed 1))))
- (t (cons :ignore (string-trim trimmed))))))
+ ((eql first-char ?+) (cons :keep (string-trim (substring trimmed 1))))
+ ((eql first-char ?-) (cons :ignore (string-trim (substring trimmed 1))))
+ ((eql first-char ?!) (cons :ensure (string-trim (substring trimmed 1))))
+ (t (cons :legacy-ignore (string-trim trimmed))))))
(defun projectile--parse-dirconfig-string (text)
"Parse TEXT (a dirconfig file's contents) into a `projectile-dirconfig'."
- (let (keep ignore ensure)
+ (let (keep ignore ensure prefixless)
(dolist (line (split-string text "\n"))
(pcase (projectile--dirconfig-classify-line line)
- (`(:keep . ,v) (unless (string-empty-p v) (push v keep)))
- (`(:ignore . ,v) (unless (string-empty-p v) (push v ignore)))
- (`(:ensure . ,v) (unless (string-empty-p v) (push v ensure)))))
+ (`(:keep . ,v) (unless (string-empty-p v) (push v keep)))
+ (`(:ignore . ,v) (unless (string-empty-p v) (push v ignore)))
+ (`(:ensure . ,v) (unless (string-empty-p v) (push v ensure)))
+ (`(:legacy-ignore . ,v) (unless (string-empty-p v)
+ (push v ignore)
+ (push v prefixless)))))
(make-projectile-dirconfig
- :keep (mapcar #'file-name-as-directory (nreverse keep))
- :ignore (nreverse ignore)
- :ensure (nreverse ensure))))
+ :keep (mapcar #'file-name-as-directory (nreverse keep))
+ :ignore (nreverse ignore)
+ :ensure (nreverse ensure)
+ :prefixless-ignore (nreverse prefixless))))
(defun projectile--parse-dirconfig-file-uncached ()
"Parse the dirconfig file without caching.
@@ -2254,6 +2277,28 @@ Return a `projectile-dirconfig' or nil if the file doesn't exist."
(projectile--warn-glob-in-keep-entry entry dirconfig)))
cfg))))
+(defun projectile--maybe-warn-prefixless-entries (project-root cfg)
+ "Warn once per session about prefix-less ignore entries in CFG for PROJECT-ROOT.
+CFG is a `projectile-dirconfig' struct."
+ (when (and projectile-warn-on-prefixless-dirconfig-lines
+ cfg
+ (projectile-dirconfig-prefixless-ignore cfg)
+ (not (gethash project-root
+ projectile--prefixless-dirconfig-warned-projects)))
+ (puthash project-root t projectile--prefixless-dirconfig-warned-projects)
+ (display-warning
+ 'projectile
+ (format "%s contains entries without a `+'/`-'/`!' prefix: %s. \
+The implicit form is treated as an ignore rule for backward \
+compatibility but is being phased out — please prefix the lines \
+explicitly. Set `projectile-warn-on-prefixless-dirconfig-lines' \
+to nil to silence this warning."
+ (expand-file-name projectile-dirconfig-file project-root)
+ (mapconcat (lambda (s) (format "`%s'" s))
+ (projectile-dirconfig-prefixless-ignore cfg)
+ ", "))
+ :warning)))
+
(defun projectile-parse-dirconfig-file ()
"Parse project ignore file and return its rules.
@@ -2280,13 +2325,17 @@ dirconfig file's modification time changes."
(project-root (projectile-project-root))
(cached (gethash project-root projectile--dirconfig-cache))
(attrs (file-attributes dirconfig))
- (mtime (when attrs (file-attribute-modification-time attrs))))
- (if (and cached mtime (equal (car cached) mtime))
- (cdr cached)
- (let ((result (projectile--parse-dirconfig-file-uncached)))
- (when mtime
- (puthash project-root (cons mtime result) projectile--dirconfig-cache))
- result))))
+ (mtime (when attrs (file-attribute-modification-time attrs)))
+ (result (if (and cached mtime (equal (car cached) mtime))
+ (cdr cached)
+ (let ((parsed (projectile--parse-dirconfig-file-uncached)))
+ (when mtime
+ (puthash project-root
+ (cons mtime parsed)
+ projectile--dirconfig-cache))
+ parsed))))
+ (projectile--maybe-warn-prefixless-entries project-root result)
+ result))
(defun projectile-expand-root (name &optional dir)
"Expand NAME to project root.
diff --git a/test/projectile-test.el b/test/projectile-test.el
index fc6116d..9516ff1 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -539,9 +539,9 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
:to-equal '(:ignore . "/build"))
(expect (projectile--dirconfig-classify-line "!/build/keepme")
:to-equal '(:ensure . "/build/keepme")))
- (it "treats prefix-less lines as ignore for backward compatibility"
+ (it "tags prefix-less lines as legacy-ignore for backward compatibility"
(expect (projectile--dirconfig-classify-line "stale-pattern")
- :to-equal '(:ignore . "stale-pattern")))
+ :to-equal '(:legacy-ignore . "stale-pattern")))
(it "skips leading whitespace before dispatch"
(expect (projectile--dirconfig-classify-line " -indented")
:to-equal '(:ignore . "indented"))
@@ -556,7 +556,7 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
;; Without a comment prefix, # is just a regular character.
(let ((projectile-dirconfig-comment-prefix nil))
(expect (projectile--dirconfig-classify-line "#may-be-a-comment")
- :to-equal '(:ignore . "#may-be-a-comment")))))
+ :to-equal '(:legacy-ignore . "#may-be-a-comment")))))
(describe "projectile-parse-dirconfig-file"
(it "parses dirconfig and returns directories to ignore and keep"
@@ -572,7 +572,11 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
"#may-be-a-comment"
"no-prefix"
"left-wspace"
- "right-wspace")))
+ "right-wspace")
+ :prefixless-ignore '("#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)
@@ -581,7 +585,10 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
:ignore '("exclude"
"no-prefix"
"left-wspace"
- "right-wspace")))))
+ "right-wspace")
+ :prefixless-ignore '("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
@@ -595,7 +602,8 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
:to-equal (make-projectile-dirconfig
:keep '("indented-include/")
:ignore '("indented-exclude" "no-prefix-indented")
- :ensure '("indented-ensure"))))
+ :ensure '("indented-ensure")
+ :prefixless-ignore '("no-prefix-indented"))))
(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
@@ -649,7 +657,8 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
:to-equal (make-projectile-dirconfig
:keep '("/src/")
:ignore '("/build" "stale-pattern")
- :ensure '("/build/keepme")))))))
+ :ensure '("/build/keepme")
+ :prefixless-ignore '("stale-pattern")))))))
(it "round-trips non-ASCII paths through the parser"
(projectile-test-with-sandbox
(projectile-test-with-files
@@ -734,6 +743,48 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(projectile-invalidate-cache nil)
(expect (gethash root projectile--dirconfig-cache) :to-be nil))))))
+(describe "prefix-less dirconfig warning"
+ (before-each
+ (clrhash projectile--dirconfig-cache)
+ (clrhash projectile--prefixless-dirconfig-warned-projects))
+ (it "warns once when a dirconfig contains prefix-less ignore lines"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("project/.projectile")
+ (let ((root (file-truename (expand-file-name "project/"))))
+ (with-temp-file (expand-file-name ".projectile" root)
+ (insert "-foo\nstale-pattern\n"))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'display-warning)
+ (let ((projectile-warn-on-prefixless-dirconfig-lines t))
+ (projectile-parse-dirconfig-file)
+ (projectile-parse-dirconfig-file))
+ (expect 'display-warning :to-have-been-called-times 1)))))
+ (it "does not warn for a fully-prefixed dirconfig"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("project/.projectile")
+ (let ((root (file-truename (expand-file-name "project/"))))
+ (with-temp-file (expand-file-name ".projectile" root)
+ (insert "-foo\n+/src\n!/build/keep\n"))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'display-warning)
+ (let ((projectile-warn-on-prefixless-dirconfig-lines t))
+ (projectile-parse-dirconfig-file))
+ (expect 'display-warning :not :to-have-been-called)))))
+ (it "does not warn when the option is disabled"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("project/.projectile")
+ (let ((root (file-truename (expand-file-name "project/"))))
+ (with-temp-file (expand-file-name ".projectile" root)
+ (insert "stale-pattern\n"))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'display-warning)
+ (let ((projectile-warn-on-prefixless-dirconfig-lines nil))
+ (projectile-parse-dirconfig-file))
+ (expect 'display-warning :not :to-have-been-called))))))
+
(describe "alien-mode dirconfig warning"
(before-each
(clrhash projectile--alien-dirconfig-warned-projects))