aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@toptal.com>2026-04-25 19:00:09 +0100
committerBozhidar Batsov <bozhidar@toptal.com>2026-04-25 19:00:09 +0100
commit5e4471b10dc59274e7608489b28897a69c585cc8 (patch)
treea25ccb579536dcba4c35066fdfa74254e5cc288d
parent54387baa2e2bd7af9866abd27f5454da71cdbd22 (diff)
Warn when alien indexing bypasses a populated .projectile
Under alien indexing the dirconfig file is silently ignored, which is the most common confusion in the issue tracker (#1322, #1075, #1534, #1941). Show a one-shot display-warning the first time we index a project where alien mode meets a non-empty .projectile. The new projectile-warn-when-dirconfig-is-ignored option lets users who already understand the trade-off silence the warning.
-rw-r--r--CHANGELOG.md1
-rw-r--r--projectile.el42
-rw-r--r--test/projectile-test.el63
3 files changed, 105 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9e3843..9b1c821 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,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.
* [#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 f0ec260..7d7d745 100644
--- a/projectile.el
+++ b/projectile.el
@@ -404,6 +404,17 @@ Similar to '#' in .gitignore files."
:type 'character
:package-version '(projectile . "2.2.0"))
+(defcustom projectile-warn-when-dirconfig-is-ignored t
+ "Whether to warn when a non-empty .projectile is bypassed by alien indexing.
+Under the `alien' indexing method, Projectile does not consult the
+project's dirconfig file at indexing time. When this option is
+non-nil, a one-time warning is shown for each project where a
+non-empty dirconfig is present alongside alien indexing, since the
+silent bypass is a frequent source of confusion."
+ :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.
@@ -653,6 +664,9 @@ project."
"Cache for parsed dirconfig files, keyed by project root.
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-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.
@@ -2288,6 +2302,30 @@ project-root for every file."
(funcall action res)
res)))
+(defun projectile--dirconfig-non-empty-p ()
+ "Return non-nil if the current project's dirconfig file has any content."
+ (let* ((dirconfig (projectile-dirconfig-file))
+ (attrs (and (projectile-file-exists-p dirconfig)
+ (file-attributes dirconfig))))
+ (and attrs (> (file-attribute-size attrs) 0))))
+
+(defun projectile--maybe-warn-dirconfig-ignored (project-root)
+ "Warn once per session that PROJECT-ROOT's dirconfig is bypassed by alien mode."
+ (when (and projectile-warn-when-dirconfig-is-ignored
+ (eq projectile-indexing-method 'alien)
+ (not (gethash project-root
+ projectile--alien-dirconfig-warned-projects))
+ (projectile--dirconfig-non-empty-p))
+ (puthash project-root t projectile--alien-dirconfig-warned-projects)
+ (display-warning
+ 'projectile
+ (format "Project %s has a non-empty %s but `projectile-indexing-method' \
+is `alien', which bypasses dirconfig filtering. Switch to `hybrid' or \
+`native' if you need those rules to apply, or set \
+`projectile-warn-when-dirconfig-is-ignored' to nil to silence this warning."
+ project-root projectile-dirconfig-file)
+ :warning)))
+
(defun projectile-project-files (project-root)
"Return a list of files for the PROJECT-ROOT."
(let (files)
@@ -2317,7 +2355,9 @@ project-root for every file."
(if (eq projectile-indexing-method 'alien)
;; In alien mode we can just skip reading
;; .projectile and find all files in the root dir.
- (projectile-dir-files-alien project-root)
+ (progn
+ (projectile--maybe-warn-dirconfig-ignored project-root)
+ (projectile-dir-files-alien project-root))
;; If a project is defined as a list of subfolders
;; then we'll have the files returned for each subfolder,
;; so they are relative to the project root.
diff --git a/test/projectile-test.el b/test/projectile-test.el
index f874a83..f460194 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -633,6 +633,69 @@ 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 "alien-mode dirconfig warning"
+ (before-each
+ (clrhash projectile--alien-dirconfig-warned-projects))
+ (it "warns once when alien indexing skips a populated .projectile"
+ (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"))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'projectile-dir-files-alien :and-return-value '("a"))
+ (spy-on 'display-warning)
+ (let ((projectile-indexing-method 'alien)
+ (projectile-enable-caching nil)
+ (projectile-warn-when-dirconfig-is-ignored t))
+ (projectile-project-files root)
+ (projectile-project-files root))
+ (expect 'display-warning :to-have-been-called-times 1)))))
+ (it "does not warn for an empty .projectile"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("project/.projectile")
+ (let ((root (file-truename (expand-file-name "project/"))))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'projectile-dir-files-alien :and-return-value '("a"))
+ (spy-on 'display-warning)
+ (let ((projectile-indexing-method 'alien)
+ (projectile-enable-caching nil)
+ (projectile-warn-when-dirconfig-is-ignored t))
+ (projectile-project-files root))
+ (expect 'display-warning :not :to-have-been-called)))))
+ (it "does not warn when the warning 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 "-foo\n"))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'projectile-dir-files-alien :and-return-value '("a"))
+ (spy-on 'display-warning)
+ (let ((projectile-indexing-method 'alien)
+ (projectile-enable-caching nil)
+ (projectile-warn-when-dirconfig-is-ignored nil))
+ (projectile-project-files root))
+ (expect 'display-warning :not :to-have-been-called)))))
+ (it "does not warn under non-alien indexing"
+ (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"))
+ (spy-on 'projectile-project-root :and-return-value root)
+ (spy-on 'projectile-get-project-directories :and-return-value '())
+ (spy-on 'display-warning)
+ (let ((projectile-indexing-method 'native)
+ (projectile-enable-caching nil)
+ (projectile-warn-when-dirconfig-is-ignored t))
+ (projectile-project-files root))
+ (expect 'display-warning :not :to-have-been-called))))))
+
(describe "projectile-get-project-directories"
(it "gets the list of project directories"
(spy-on 'projectile-project-root :and-return-value "/my/root/")