branch: elpa/projectile
commit 6449fb0465587c757c1fbf1a64bb4ba420a78bfa
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>

    Derive :project-file from the first marker file
    
    Every file-based project type repeated its primary manifest twice: once
    as the first marker file and again as :project-file (the value that
    seeds projectile-project-root-files). The two were identical in all 53
    cases, so a new type meant updating two spots in lockstep.
    
    Default :project-file to the first marker file when it isn't given
    explicitly, and drop the now-redundant declarations. Function-marker
    types (and the handful whose manifest is a wildcard or genuinely differs
    from the first marker) keep their explicit :project-file.
    
    Add a `none' opt-out for the one type that must not derive one: bloop's
    marker, .bloop/bloop.settings.json, is also dropped in $HOME by the
    bloop server, so seeding it as a root marker would index the home
    directory (the regression #1901 originally fixed by removing bloop from
    the root-file list). `none' detects the type without anchoring a root.
---
 doc/modules/ROOT/pages/projects.adoc |  11 ++--
 projectile.el                        | 105 +++++++++++++----------------------
 test/projectile-test.el              |  38 ++++++++++++-
 3 files changed, 81 insertions(+), 73 deletions(-)

diff --git a/doc/modules/ROOT/pages/projects.adoc 
b/doc/modules/ROOT/pages/projects.adoc
index 404cbd1ab2..616259989c 100644
--- a/doc/modules/ROOT/pages/projects.adoc
+++ b/doc/modules/ROOT/pages/projects.adoc
@@ -298,7 +298,7 @@ What this does is:
 
 . add your own type of project, in this case `npm` package.
 . add a list of files and/or folders in a root of the project that helps to 
identify the type, in this case it is only `package.json`. This can also be a 
function which takes a project root as argument and verifies whether that 
directory has the correct project structure for the type.
-. add _project-file_, which is typically the primary project configuration 
file. In this case that's `package.json`. The value can contain wildcards 
and/or be a list containing multiple project files to look for.
+. add _project-file_, which is typically the primary project configuration 
file. In this case that's `package.json`. When omitted it defaults to the first 
marker file (so the explicit `:project-file` above is optional here); supply it 
explicitly when the marker is a function or when the primary file differs from 
the first marker. The value can contain wildcards and/or be a list containing 
multiple project files to look for. Pass the symbol `none` for a type that 
should be detected but ne [...]
 . add _compile-command_, in this case it is `npm install`.
 . add _test-command_, in this case it is `npm test`.
 . add _run-command_, in this case it is `npm start`.
@@ -769,10 +769,11 @@ precedence with respect to project detection. Let's 
examine the defaults:
 * `projectile-root-top-down` is similar, but it will return the top-most
   (farthest from the current directory) match. It is configurable via
   `projectile-project-root-files`, which is populated automatically from the
-  project types registered via `projectile-register-project-type`. It is
-  consulted after the bottom-up search fails, so it's where the per-language
-  manifests (`deps.edn`, `Cargo.toml`, `pom.xml`, `?*.csproj`, ...) actually
-  get matched.
+  project types registered via `projectile-register-project-type` (each
+  type's `:project-file`, or the first of its marker files when no
+  `:project-file` is given). It is consulted after the bottom-up search
+  fails, so it's where the per-language manifests (`deps.edn`, `Cargo.toml`,
+  `pom.xml`, `?*.csproj`, ...) actually get matched.
 
 * `projectile-root-top-down-recurring` will look for project markers that can
   appear at every level of a project (e.g. `Makefile` or `.svn`) and will 
return
diff --git a/projectile.el b/projectile.el
index 338d371dc5..fa2af032aa 100644
--- a/projectile.el
+++ b/projectile.el
@@ -3577,7 +3577,11 @@ ones and overrule settings in the other lists."
 A project type is defined by PROJECT-TYPE, a set of MARKER-FILES,
 and optional keyword arguments:
 PROJECT-FILE the main project file in the root project directory.  It may be a
-             single file or a list of possible files.
+             single file or a list of possible files.  When omitted it
+             defaults to the first marker file.  Pass the symbol `none'
+             to opt out, so the type is detected but contributes no
+             project-root marker (e.g. when its marker also appears
+             outside real projects).
 COMPILATION-DIR the directory to run the tests- and compilations in,
 CONFIGURE which specifies a command that configures the project
           `%s' in the command will be substituted with 
(projectile-project-root)
@@ -3597,18 +3601,30 @@ files such as test/impl/other files as below:
     returns a plist containing :test, :impl or :other as key and the
     relative path/paths or predicate as value.  PREDICATE accepts a
     relative path as the input."
-  (let ((project-plist (list 'marker-files marker-files
-                             'project-file project-file
-                             'compilation-dir compilation-dir
-                             'configure-command configure
-                             'compile-command compile
-                             'test-command test
-                             'install-command install
-                             'package-command package
-                             'run-command run))
-        (project-files (if (listp project-file)
-                           project-file
-                         (list project-file))))
+  ;; When PROJECT-FILE isn't given explicitly, derive it from the first
+  ;; marker file - that's the project's primary manifest in every
+  ;; file-based registration, so callers needn't repeat it.  Function
+  ;; markers (a symbol or lambda) have no list to derive from.  Passing
+  ;; the symbol `none' opts out of both the derivation and the root-file
+  ;; seeding below, for types (e.g. bloop) whose only marker also shows
+  ;; up outside real projects and so must not anchor a project root.
+  (let* ((project-file (cond ((eq project-file 'none) nil)
+                             (project-file project-file)
+                             ((and (consp marker-files)
+                                   (stringp (car marker-files)))
+                              (car marker-files))))
+         (project-plist (list 'marker-files marker-files
+                              'project-file project-file
+                              'compilation-dir compilation-dir
+                              'configure-command configure
+                              'compile-command compile
+                              'test-command test
+                              'install-command install
+                              'package-command package
+                              'run-command run))
+         (project-files (if (listp project-file)
+                            project-file
+                          (list project-file))))
     (dolist (project-file project-files)
       (when (and project-file (not (member project-file 
projectile-project-root-files)))
         (add-to-list 'projectile-project-root-files project-file)))
@@ -3631,7 +3647,11 @@ files such as test/impl/other files as below:
 A project type is defined by PROJECT-TYPE, a set of MARKER-FILES,
 and optional keyword arguments:
 PROJECT-FILE the main project file in the root project directory.  It may be a
-             single file or a list of possible files.
+             single file or a list of possible files.  When omitted it
+             defaults to the first marker file.  Pass the symbol `none'
+             to opt out, so the type is detected but contributes no
+             project-root marker (e.g. when its marker also appears
+             outside real projects).
 COMPILATION-DIR the directory to run the tests- and compilations in,
 CONFIGURE which specifies a command that configures the project
           `%s' in the command will be substituted with 
(projectile-project-root)
@@ -4035,53 +4055,43 @@ a manual COMMAND-TYPE command is created with
 
 ;; Universal
 (projectile-register-project-type 'xmake '("xmake.lua")
-                                  :project-file "xmake.lua"
                                   :compile "xmake build"
                                   :test "xmake test"
                                   :run "xmake run"
                                   :install "xmake install")
 (projectile-register-project-type 'scons '("SConstruct")
-                                  :project-file "SConstruct"
                                   :compile "scons"
                                   :test "scons test"
                                   :test-suffix "test")
 (projectile-register-project-type 'meson '("meson.build")
-                                  :project-file "meson.build"
                                   :compilation-dir "build"
                                   :configure "meson %s"
                                   :compile "ninja"
                                   :test "ninja test")
 (projectile-register-project-type 'nix '("default.nix")
-                                  :project-file "default.nix"
                                   :compile "nix-build"
                                   :test "nix-build")
 (projectile-register-project-type 'nix-flake '("flake.nix")
-                                  :project-file "flake.nix"
                                   :compile "nix build"
                                   :test "nix flake check"
                                   :run "nix run")
 (projectile-register-project-type 'bazel '("WORKSPACE")
-                                  :project-file "WORKSPACE"
                                   :compile "bazel build"
                                   :test "bazel test"
                                   :run "bazel run")
 (projectile-register-project-type 'debian '("debian/control")
-                                  :project-file "debian/control"
                                   :compile "debuild -uc -us")
 
 ;; Make & CMake
 (projectile-register-project-type 'make '("Makefile")
-                                  :project-file "Makefile"
                                   :compile "make"
                                   :test "make test"
                                   :install "make install")
 (projectile-register-project-type 'gnumake '("GNUMakefile")
-                                  :project-file "GNUMakefile"
                                   :compile "make"
                                   :test "make test"
                                   :install "make install")
 (projectile-register-project-type 'cmake '("CMakeLists.txt")
-                                  :project-file "CMakeLists.txt"
                                   :configure 
#'projectile--cmake-configure-command
                                   :compile #'projectile--cmake-compile-command
                                   :test #'projectile--cmake-test-command
@@ -4089,7 +4099,6 @@ a manual COMMAND-TYPE command is created with
                                   :package #'projectile--cmake-package-command)
 ;; go-task/task
 (projectile-register-project-type 'go-task '("Taskfile.yml")
-                                  :project-file "Taskfile.yml"
                                   :compile "task build"
                                   :test "task test"
                                   :install "task install")
@@ -4100,122 +4109,101 @@ a manual COMMAND-TYPE command is created with
                                   :test-suffix "_test")
 ;; PHP
 (projectile-register-project-type 'php-symfony '("composer.json" "app" "src" 
"vendor")
-                                  :project-file "composer.json"
                                   :compile "app/console server:run"
                                   :test "phpunit -c app "
                                   :test-suffix "Test")
 ;; Erlang & Elixir
 (projectile-register-project-type 'rebar '("rebar.config")
-                                  :project-file "rebar.config"
                                   :compile "rebar3 compile"
                                   :test "rebar3 do eunit,ct"
                                   :test-suffix "_SUITE")
 (projectile-register-project-type 'elixir '("mix.exs")
-                                  :project-file "mix.exs"
                                   :compile "mix compile"
                                   :src-dir "lib/"
                                   :test "mix test"
                                   :test-suffix "_test")
 ;; JavaScript
 (projectile-register-project-type 'grunt '("Gruntfile.js")
-                                  :project-file "Gruntfile.js"
                                   :compile "grunt"
                                   :test "grunt test")
 (projectile-register-project-type 'gulp '("gulpfile.js")
-                                  :project-file "gulpfile.js"
                                   :compile "gulp"
                                   :test "gulp test")
 (projectile-register-project-type 'npm '("package.json" "package-lock.json")
-                                  :project-file "package.json"
                                   :compile "npm install && npm run build"
                                   :test "npm test"
                                   :test-suffix ".test")
 (projectile-register-project-type 'yarn '("package.json" "yarn.lock")
-                                  :project-file "package.json"
                                   :compile "yarn && yarn build"
                                   :test "yarn test"
                                   :test-suffix ".test")
 (projectile-register-project-type 'pnpm '("package.json" "pnpm-lock.yaml")
-                                  :project-file "package.json"
                                   :compile "pnpm install && pnpm build"
                                   :test "pnpm test"
                                   :test-suffix ".test")
 ;; Angular
 (projectile-register-project-type 'angular '("angular.json" 
".angular-cli.json")
-                                  :project-file "angular.json"
                                   :compile "ng build"
                                   :run "ng serve"
                                   :test "ng test"
                                   :test-suffix ".spec")
 ;; Python
 (projectile-register-project-type 'django '("manage.py")
-                                  :project-file "manage.py"
                                   :compile "python manage.py runserver"
                                   :test "python manage.py test"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 (projectile-register-project-type 'python-pip '("requirements.txt")
-                                  :project-file "requirements.txt"
                                   :compile "python setup.py build"
                                   :test "python -m unittest discover"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 (projectile-register-project-type 'python-pkg '("setup.py")
-                                  :project-file "setup.py"
                                   :compile "python setup.py build"
                                   :test "python -m unittest discover"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 (projectile-register-project-type 'python-tox '("tox.ini")
-                                  :project-file "tox.ini"
                                   :compile "tox -r --notest"
                                   :test "tox"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 (projectile-register-project-type 'python-pipenv '("Pipfile")
-                                  :project-file "Pipfile"
                                   :compile "pipenv run build"
                                   :test "pipenv run test"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 (projectile-register-project-type 'python-poetry '("poetry.lock")
-                                  :project-file "poetry.lock"
                                   :compile "poetry build"
                                   :test "poetry run python -m unittest 
discover"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 (projectile-register-project-type 'python-toml '("pyproject.toml")
-                                  :project-file "pyproject.toml"
                                   :compile "python -m build"
                                   :test "python -m unittest discover"
                                   :test-prefix "test_"
                                   :test-suffix "_test")
 ;; Java & friends
 (projectile-register-project-type 'maven '("pom.xml")
-                                  :project-file "pom.xml"
                                   :compile "mvn -B clean install"
                                   :test "mvn -B test"
                                   :test-suffix "Test"
                                   :src-dir "src/main/"
                                   :test-dir "src/test/")
 (projectile-register-project-type 'gradle '("build.gradle")
-                                  :project-file "build.gradle"
                                   :compile "gradle build"
                                   :test "gradle test"
                                   :test-suffix "Spec")
 (projectile-register-project-type 'gradlew '("gradlew")
-                                  :project-file "gradlew"
                                   :compile "./gradlew build"
                                   :test "./gradlew test"
                                   :test-suffix "Spec")
 (projectile-register-project-type 'grails '("application.yml" "grails-app")
-                                  :project-file "application.yml"
                                   :compile "grails package"
                                   :test "grails test-app"
                                   :test-suffix "Spec")
 ;; Scala
 (projectile-register-project-type 'sbt '("build.sbt")
-                                  :project-file "build.sbt"
                                   :src-dir "main"
                                   :test-dir "test"
                                   :compile "sbt compile"
@@ -4230,7 +4218,11 @@ a manual COMMAND-TYPE command is created with
                                   :test "mill __.test"
                                   :test-suffix "Test")
 
+;; Bloop drops a `.bloop/bloop.settings.json' in the project, but its
+;; server also keeps one in `$HOME', so the marker must not anchor a
+;; project root (see #1901) - only drive type detection.
 (projectile-register-project-type 'bloop '(".bloop/bloop.settings.json")
+                                  :project-file 'none
                                   :compile "bloop compile root"
                                   :test "bloop test --propagate --reporter 
scalac root"
                                   :src-dir "src/main/"
@@ -4239,34 +4231,28 @@ a manual COMMAND-TYPE command is created with
 
 ;; Clojure
 (projectile-register-project-type 'lein-test '("project.clj")
-                                  :project-file "project.clj"
                                   :compile "lein compile"
                                   :test "lein test"
                                   :test-suffix "_test")
 (projectile-register-project-type 'lein-midje '("project.clj" ".midje.clj")
-                                  :project-file "project.clj"
                                   :compile "lein compile"
                                   :test "lein midje"
                                   :test-prefix "t_")
 (projectile-register-project-type 'boot-clj '("build.boot")
-                                  :project-file "build.boot"
                                   :compile "boot aot"
                                   :test "boot test"
                                   :test-suffix "_test")
 (projectile-register-project-type 'clojure-cli '("deps.edn")
-                                  :project-file "deps.edn"
                                   :test-suffix "_test")
 
 ;; Ruby
 (projectile-register-project-type 'ruby-rspec '("Gemfile" "lib" "spec")
-                                  :project-file "Gemfile"
                                   :compile "bundle exec rake"
                                   :src-dir "lib/"
                                   :test "bundle exec rspec"
                                   :test-dir "spec/"
                                   :test-suffix "_spec")
 (projectile-register-project-type 'ruby-test '("Gemfile" "lib" "test")
-                                  :project-file "Gemfile"
                                   :compile "bundle exec rake"
                                   :src-dir "lib/"
                                   :test "bundle exec rake test"
@@ -4274,13 +4260,11 @@ a manual COMMAND-TYPE command is created with
 ;; Rails needs to be registered after npm, otherwise `package.json` makes it 
`npm`.
 ;; https://github.com/bbatsov/projectile/pull/1191
 (projectile-register-project-type 'rails-test '("Gemfile" "app" "lib" "db" 
"config" "test")
-                                  :project-file "Gemfile"
                                   :compile "bundle exec rails server"
                                   :src-dir "app/"
                                   :test "bundle exec rake test"
                                   :test-suffix "_test")
 (projectile-register-project-type 'rails-rspec '("Gemfile" "app" "lib" "db" 
"config" "spec")
-                                  :project-file "Gemfile"
                                   :compile "bundle exec rails server"
                                   :src-dir "app/"
                                   :test "bundle exec rspec"
@@ -4288,7 +4272,6 @@ a manual COMMAND-TYPE command is created with
                                   :test-suffix "_spec")
 ;; Crystal
 (projectile-register-project-type 'crystal-spec '("shard.yml")
-                                  :project-file "shard.yml"
                                   :src-dir "src/"
                                   :test "crystal spec"
                                   :test-dir "spec/"
@@ -4296,13 +4279,11 @@ a manual COMMAND-TYPE command is created with
 
 ;; Emacs
 (projectile-register-project-type 'emacs-cask '("Cask")
-                                  :project-file "Cask"
                                   :compile "cask install"
                                   :test-prefix "test-"
                                   :test-suffix "-test")
 
 (projectile-register-project-type 'emacs-eask '("Eask")
-                                  :project-file "Eask"
                                   :compile "eask install"
                                   :test-prefix "test-"
                                   :test-suffix "-test")
@@ -4316,34 +4297,29 @@ a manual COMMAND-TYPE command is created with
 
 ;; R
 (projectile-register-project-type 'r '("DESCRIPTION")
-                                  :project-file "DESCRIPTION"
                                   :compile "R CMD INSTALL --with-keep.source ."
                                   :test (concat "R CMD check -o " 
temporary-file-directory " ."))
 
 ;; Haskell
 (projectile-register-project-type 'haskell-stack '("stack.yaml")
-                                  :project-file "stack.yaml"
                                   :compile "stack build"
                                   :test "stack build --test"
                                   :test-suffix "Spec")
 
 ;; Rust
 (projectile-register-project-type 'rust-cargo '("Cargo.toml")
-                                  :project-file "Cargo.toml"
                                   :compile "cargo build"
                                   :test "cargo test"
                                   :run "cargo run")
 
 ;; Racket
 (projectile-register-project-type 'racket '("info.rkt")
-                                  :project-file "info.rkt"
                                   :test "raco test ."
                                   :install "raco pkg install"
                                   :package "raco pkg create --source $(pwd)")
 
 ;; Dart
 (projectile-register-project-type 'dart '("pubspec.yaml")
-                                  :project-file "pubspec.yaml"
                                   :compile "pub get"
                                   :test "pub run test"
                                   :run "dart"
@@ -4351,12 +4327,10 @@ a manual COMMAND-TYPE command is created with
 
 ;; Elm
 (projectile-register-project-type 'elm '("elm.json")
-                                  :project-file "elm.json"
                                   :compile "elm make")
 
 ;; Julia
 (projectile-register-project-type 'julia '("Project.toml")
-                                  :project-file "Project.toml"
                                   :compile "julia --project=@. -e 'import Pkg; 
Pkg.precompile(); Pkg.build()'"
                                   :test "julia --project=@. -e 'import Pkg; 
Pkg.test()' --check-bounds=yes"
                                   :src-dir "src"
@@ -4364,20 +4338,17 @@ a manual COMMAND-TYPE command is created with
 
 ;; OCaml
 (projectile-register-project-type 'ocaml-dune '("dune-project")
-                                  :project-file "dune-project"
                                   :compile "dune build"
                                   :test "dune runtest")
 
 ;; Zig
 (projectile-register-project-type 'zig '("build.zig.zon")
-                                  :project-file "build.zig.zon"
                                   :compile "zig build"
                                   :test "zig build test"
                                   :run "zig build run")
 
 ;; Swift
 (projectile-register-project-type 'swift-spm '("Package.swift")
-                                  :project-file "Package.swift"
                                   :compile "swift build"
                                   :test "swift test"
                                   :run "swift run")
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 5bbc0ed81e..e25a95e92a 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -223,7 +223,43 @@ by `projectile-files-via-ext-command')."
     (projectile-register-project-type 'foo '("Foo"))
     (expect (caar projectile-project-types) :to-equal 'foo)
     (projectile-register-project-type 'bar '("Bar"))
-    (expect (caar projectile-project-types) :to-equal 'bar)))
+    (expect (caar projectile-project-types) :to-equal 'bar))
+
+  (it "derives project-file from the first marker file and seeds the top-down 
list"
+    (let ((projectile-project-types nil)
+          (projectile-project-root-files nil)
+          (projectile-project-root-files-bottom-up '(".git")))
+      (projectile-register-project-type 'foo '("foo.manifest" "extra-dir"))
+      (expect (projectile-project-type-attribute 'foo 'project-file)
+              :to-equal "foo.manifest")
+      ;; only the first marker is treated as the project file
+      (expect (member "foo.manifest" projectile-project-root-files) 
:to-be-truthy)
+      (expect (member "extra-dir" projectile-project-root-files) :not 
:to-be-truthy)
+      ;; manifests never leak into the bottom-up list (VCS markers win)
+      (expect (member "foo.manifest" projectile-project-root-files-bottom-up)
+              :not :to-be-truthy)))
+
+  (it "honors an explicit project-file over the first marker file"
+    (let ((projectile-project-types nil)
+          (projectile-project-root-files nil))
+      (projectile-register-project-type 'foo '("foo.manifest") :project-file 
"real.manifest")
+      (expect (projectile-project-type-attribute 'foo 'project-file)
+              :to-equal "real.manifest")
+      (expect (member "real.manifest" projectile-project-root-files) 
:to-be-truthy)
+      (expect (member "foo.manifest" projectile-project-root-files) :not 
:to-be-truthy)))
+
+  (it "opts out of root-file seeding when project-file is `none'"
+    ;; Regression for #1901: bloop's marker also lives in $HOME, so it
+    ;; must drive detection without ever anchoring a project root.
+    (let ((projectile-project-types nil)
+          (projectile-project-root-files nil)
+          (projectile-project-root-files-bottom-up '(".git")))
+      (projectile-register-project-type 'foo '(".foo/settings.json") 
:project-file 'none)
+      (expect (projectile-project-type-attribute 'foo 'project-file) :to-equal 
nil)
+      (expect (member ".foo/settings.json" projectile-project-root-files)
+              :not :to-be-truthy)
+      (expect (member ".foo/settings.json" 
projectile-project-root-files-bottom-up)
+              :not :to-be-truthy))))
 
 (describe "projectile-update-project-type"
   :var ((mock-projectile-project-types

Reply via email to