From cb498de41d986a168f7449528926d789e0cc5d41 Mon Sep 17 00:00:00 2001 From: it-education-md <128720033+it-education-md@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:11:57 -0500 Subject: [PATCH] [2.x] fix: Fixes plugin toggle precedence between disablePlugins and enablePlugins (#8794) Treat explicit plugin toggles as last-call-wins for the same plugin. This avoids contradictory include/exclude states when disablePlugins(X) is followed by enablePlugins(X) (and vice versa), aligning behavior with normal override expectations. Apply the same semantics to ProjectMatrix and add regression coverage: - unit tests in main/src/test/scala/ProjectSpec.scala - scripted test in sbt-app/src/sbt-test/project/i1926-disable-enable-plugin --- .../src/main/scala/sbt/Plugins.scala | 10 ++++++++++ .../src/main/scala/sbt/Project.scala | 11 ++++++++-- main/src/main/scala/sbt/ProjectMatrix.scala | 11 ++++++++-- main/src/test/scala/ProjectSpec.scala | 20 +++++++++++++++++++ .../broken.sbt.disabled | 4 ++++ .../i1926-disable-enable-plugin/build.sbt | 4 ++++ .../project/Issue1926Plugin.scala | 13 ++++++++++++ .../project/i1926-disable-enable-plugin/test | 4 ++++ 8 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/broken.sbt.disabled create mode 100644 sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/build.sbt create mode 100644 sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/project/Issue1926Plugin.scala create mode 100644 sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/test diff --git a/main-settings/src/main/scala/sbt/Plugins.scala b/main-settings/src/main/scala/sbt/Plugins.scala index d1e2f8142..f9357cc60 100644 --- a/main-settings/src/main/scala/sbt/Plugins.scala +++ b/main-settings/src/main/scala/sbt/Plugins.scala @@ -343,6 +343,16 @@ ${listConflicts(conflicting)}""") case And(ns) => ns.foldLeft(a)(_ && _) case b: Basic => a && b } + + private[sbt] def overrideWith(current: Plugins, update: Plugins): Plugins = { + val opposite: Set[Basic] = flatten(update).map { + case Exclude(p) => p: Basic + case p: AutoPlugin => + Exclude(p): Basic + }.toSet + and(remove(current, opposite), update) + } + private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match { case b: Basic => if (del(b)) Empty else b case Empty => Empty diff --git a/main-settings/src/main/scala/sbt/Project.scala b/main-settings/src/main/scala/sbt/Project.scala index b74c057d9..f89beb908 100644 --- a/main-settings/src/main/scala/sbt/Project.scala +++ b/main-settings/src/main/scala/sbt/Project.scala @@ -158,11 +158,18 @@ sealed trait Project extends ProjectDefinition[ProjectReference] with CompositeP * A [[AutoPlugin]] is a common label that is used by plugins to determine what settings, if any, to enable on a project. */ def enablePlugins(ns: Plugins*): Project = - setPlugins(ns.foldLeft(plugins)(Plugins.and)) + setPlugins(ns.foldLeft(plugins)(Plugins.overrideWith)) /** Disable the given plugins on this project. */ def disablePlugins(ps: AutoPlugin*): Project = - setPlugins(Plugins.and(plugins, Plugins.And(ps.map(p => Plugins.Exclude(p)).toList))) + if ps.isEmpty then this + else + setPlugins( + Plugins.overrideWith( + plugins, + Plugins.And(ps.map(p => Plugins.Exclude(p)).toList) + ) + ) private[sbt] def setPlugins(ns: Plugins): Project = copy(plugins = ns) diff --git a/main/src/main/scala/sbt/ProjectMatrix.scala b/main/src/main/scala/sbt/ProjectMatrix.scala index d4d172d1e..ecb44a548 100644 --- a/main/src/main/scala/sbt/ProjectMatrix.scala +++ b/main/src/main/scala/sbt/ProjectMatrix.scala @@ -455,10 +455,17 @@ object ProjectMatrix { copy(settings = (settings: Seq[Def.Setting[?]]) ++ Def.settings(ss*)) override def enablePlugins(ns: Plugins*): ProjectMatrix = - setPlugins(ns.foldLeft(plugins)(Plugins.and)) + setPlugins(ns.foldLeft(plugins)(Plugins.overrideWith)) override def disablePlugins(ps: AutoPlugin*): ProjectMatrix = - setPlugins(Plugins.and(plugins, Plugins.And(ps.map(p => Plugins.Exclude(p)).toList))) + if ps.isEmpty then this + else + setPlugins( + Plugins.overrideWith( + plugins, + Plugins.And(ps.map(p => Plugins.Exclude(p)).toList) + ) + ) override def configure(ts: (Project => Project)*): ProjectMatrix = copy(transforms = transforms ++ ts) diff --git a/main/src/test/scala/ProjectSpec.scala b/main/src/test/scala/ProjectSpec.scala index e6490d1aa..cd8824124 100644 --- a/main/src/test/scala/ProjectSpec.scala +++ b/main/src/test/scala/ProjectSpec.scala @@ -8,10 +8,30 @@ package sbt +import java.io.File + object ProjectSpec extends verify.BasicTestSuite { + object TestPlugin extends AutoPlugin { + override def requires: Plugins = empty + } + + private val base = new File(".") + test("Project should normalize projectIDs if they are empty") { assert(Project.normalizeProjectID(emptyFilename) == Right("root")) } + test("disablePlugins then enablePlugins should keep plugin enabled") { + val p = Project("test", base).disablePlugins(TestPlugin).enablePlugins(TestPlugin) + assert(Plugins.hasInclude(p.plugins, TestPlugin)) + assert(!Plugins.hasExclude(p.plugins, TestPlugin)) + } + + test("enablePlugins then disablePlugins should keep plugin disabled") { + val p = Project("test", base).enablePlugins(TestPlugin).disablePlugins(TestPlugin) + assert(!Plugins.hasInclude(p.plugins, TestPlugin)) + assert(Plugins.hasExclude(p.plugins, TestPlugin)) + } + def emptyFilename = "" } diff --git a/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/broken.sbt.disabled b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/broken.sbt.disabled new file mode 100644 index 000000000..516482b4f --- /dev/null +++ b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/broken.sbt.disabled @@ -0,0 +1,4 @@ +def myProject(id: String): Project = + Project(id, file(id)).disablePlugins(Issue1926Plugin) + +lazy val b = myProject("b").enablePlugins(Issue1926Plugin) diff --git a/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/build.sbt b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/build.sbt new file mode 100644 index 000000000..1aae0a282 --- /dev/null +++ b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/build.sbt @@ -0,0 +1,4 @@ +def myProject(id: String): Project = + Project(id, file(id)).disablePlugins(Issue1926Plugin) + +lazy val a = myProject("a") diff --git a/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/project/Issue1926Plugin.scala b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/project/Issue1926Plugin.scala new file mode 100644 index 000000000..13d9bc1b2 --- /dev/null +++ b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/project/Issue1926Plugin.scala @@ -0,0 +1,13 @@ +import sbt._ + +object Issue1926Plugin extends AutoPlugin { + object autoImport { + val issue1926Marker = settingKey[String]("Marker setting for issue #1926 test") + } + + import autoImport._ + + override def requires: Plugins = plugins.JvmPlugin + override def trigger = noTrigger + override def projectSettings = Seq(issue1926Marker := "enabled") +} diff --git a/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/test b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/test new file mode 100644 index 000000000..9af23452f --- /dev/null +++ b/sbt-app/src/sbt-test/project/i1926-disable-enable-plugin/test @@ -0,0 +1,4 @@ +> projects +$ copy-file broken.sbt.disabled broken.sbt +> reload +> show b/issue1926Marker