Skip to content

Don't let-bind default-directory across switch-project-action#2006

Open
natetarrh wants to merge 1 commit into
bbatsov:masterfrom
natetarrh:natetarrh/fix-switch-project-default-directory-leak
Open

Don't let-bind default-directory across switch-project-action#2006
natetarrh wants to merge 1 commit into
bbatsov:masterfrom
natetarrh:natetarrh/fix-switch-project-default-directory-leak

Conversation

@natetarrh
Copy link
Copy Markdown

Summary

projectile-switch-project-by-name wraps the call to projectile-switch-project-action in (let* ((default-directory project-to-switch)) ...). Because default-directory is auto-buffer-local, the let-binding mutates the calling buffer's buffer-local default-directory for the duration of the action.

Switch-project actions that iterate (buffer-list) and read each buffer's default-directory via with-current-buffer then misclassify the originating buffer as belonging to the target project. The simplest way to hit this is setting

(setq projectile-switch-project-action #'projectile-switch-to-buffer)

…and pressing s-p p from any buffer that isn't already in the target project: that buffer shows up as a "buffer of the target project" in the picker, even though it isn't, because projectile-project-buffer-p reads its default-directory while the let-binding is in scope and finds the target root.

I hit this with projectile-switch-to-buffer from a local vterm into a /docker: TRAMP project — the local vterm's (file-remote-p default-directory) transiently equaled (file-remote-p "/docker:dev-in-docker:/src/") and the buffer got grouped with the docker project's buffers. I traced it via add-variable-watcher on default-directory; the final unlet event reverts the originating buffer's value, with the call stack rooted in projectile-switch-project-by-name.

Fix

Set default-directory on the temporary buffer the action runs in instead of let-binding it in the caller. Same effect (action body sees default-directory = target root) without the leak.

-    (let* ((default-directory project-to-switch)
-           (switched-buffer
-            (with-temp-buffer
-              (hack-dir-local-variables-non-file-buffer)
-              ...)))
+    (let ((switched-buffer
+           (with-temp-buffer
+             (setq-local default-directory project-to-switch)
+             (hack-dir-local-variables-non-file-buffer)
+             ...)))

Nothing else in projectile-switch-project-by-name reads default-directory outside the with-temp-buffer, so the change is local. All 326 existing specs pass.

Test

Added a buttercup regression test that:

  1. Creates an "origin" buffer with default-directory set to a known dir.
  2. Sets projectile-switch-project-action to a lambda that reads the origin buffer's default-directory while the action is running.
  3. Asserts that what the action observed equals the original default-directory, not the project being switched to.

Confirmed the test fails on master (observes project/) and passes after the fix (observes origin/).

Notes

  • The fix is intentionally minimal — no behaviour change for actions that depend on default-directory being the target root, since the temp buffer still carries that value.
  • The let* was load-bearing only for the default-directory it set; with that moved into with-temp-buffer, it collapses to a plain let of one binding.
  • I'd argue any switch-project-action that explicitly wants the caller's buffer mutated is unusual enough that this is a strict improvement; happy to be talked out of it though.

projectile-switch-project-by-name wrapped the action call in
(let* ((default-directory project-to-switch)) ...). Because
default-directory is auto-buffer-local, the let-binding mutated the
calling buffer's local default-directory for the duration of the
action.

When a switch-project-action iterates (buffer-list) and reads each
buffer's default-directory via with-current-buffer (e.g.
projectile-switch-to-buffer set as projectile-switch-project-action,
or any custom action built around projectile-project-buffers), the
originating buffer's transiently-mutated default-directory matched
the target project's root and the buffer was misclassified as a
project buffer.

Set default-directory on the temporary buffer the action runs in
instead. Same effect (action runs with default-directory pointing at
the target root) without leaking into the caller. Adds a buttercup
regression test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant