From 063ff5e5b073f222ec26e18850756301b340d7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 28 Oct 2022 12:46:58 +0200 Subject: [PATCH] Fix #102: Do not overwrite other plugins' `scalacOptions`. We now maintain a set of `ScalacOptions` "managed" by sbt-tpolecat. When computing `scalacOptions`, we do not overwrite them, but instead we get the previous `scalacOptions.value` and we modify it. We only remove options that are managed by sbt-tpolecat. And we only add options that are not already there (because they were already added in an upper delegate scope). By default, we automatically compute the set of managed options as all the options that are "ever" added once by sbt-tpolecat in the delegate chain. --- .../davidgregory084/TpolecatPlugin.scala | 73 +++++++++++++++++-- .../sbt-tpolecat/scalacOptions/build.sbt | 29 ++++++++ .../scalacOptions/project/OtherPlugin.scala | 27 +++++++ .../sbt-test/sbt-tpolecat/scalacOptions/test | 3 + 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 plugin/src/sbt-test/sbt-tpolecat/scalacOptions/project/OtherPlugin.scala diff --git a/plugin/src/main/scala/io/github/davidgregory084/TpolecatPlugin.scala b/plugin/src/main/scala/io/github/davidgregory084/TpolecatPlugin.scala index 566ef65..9de10b4 100644 --- a/plugin/src/main/scala/io/github/davidgregory084/TpolecatPlugin.scala +++ b/plugin/src/main/scala/io/github/davidgregory084/TpolecatPlugin.scala @@ -29,11 +29,11 @@ object TpolecatPlugin extends AutoPlugin { object autoImport { object ScalacOptions extends ScalacOptions - def scalacOptionsFor( + private[TpolecatPlugin] def supportedOptionsFor( version: String, modeScalacOptions: Set[ScalacOption] - ): Seq[String] = { - val supportedOptions = (CrossVersion.partialVersion(version), version.split('.')) match { + ): Set[ScalacOption] = { + (CrossVersion.partialVersion(version), version.split('.')) match { case (Some((0, _)), _) => // dotty prereleases use 0 as major version modeScalacOptions .filter(_.isSupported(V3_0_0)) // treat dotty prereleases as 3.0.0 @@ -47,8 +47,13 @@ object TpolecatPlugin extends AutoPlugin { case (None, _) => Set.empty[ScalacOption] } + } - supportedOptions.toList.flatMap(opt => opt.option :: opt.args) + def scalacOptionsFor( + version: String, + modeScalacOptions: Set[ScalacOption] + ): Seq[String] = { + supportedOptionsFor(version, modeScalacOptions).toList.flatMap(opt => opt.option :: opt.args) } val tpolecatDefaultOptionsMode = settingKey[OptionsMode]( @@ -90,6 +95,14 @@ object TpolecatPlugin extends AutoPlugin { val tpolecatExcludeOptions = settingKey[Set[ScalacOption]]( "The set of scalac options that will be excluded." ) + + val tpolecatEffectiveScalacOptions = settingKey[Set[ScalacOption]]( + "The set of scalac options that will effectively be applied by the sbt-tpolecat. For internal use only." + ).withRank(sbt.KeyRanks.Invisible) + + val tpolecatManagedScalacOptions = settingKey[Set[ScalacOption]]( + "The set of scalac options that sbt-tpolecat owns and manages. Defaults to anything it ever adds in any scope delegation chain." + ).withRank(sbt.KeyRanks.DSetting) } import autoImport._ @@ -122,13 +135,60 @@ object TpolecatPlugin extends AutoPlugin { tpolecatDevModeOptions := ScalacOptions.default ) ++ commandAliases + private def removeOption(options: List[String], optionToRemove: ScalacOption): List[String] = { + val option = optionToRemove.option + val args = optionToRemove.args + + if (args.isEmpty) { + // fast path + options.filterNot(_ == option) + } else { + def loop(options: List[String]): List[String] = options match { + case Nil => Nil + case `option` :: tail if tail.startsWith(args) => loop(tail.drop(args.size)) + case head :: tail => head :: loop(tail) + } + loop(options) + } + } + + private def addOption(options: List[String], optionToAdd: ScalacOption): List[String] = { + val option = optionToAdd.option + val args = optionToAdd.args + + if (args.isEmpty) { + // fast path + if (options.contains(option)) options + else options ::: option :: Nil + } else { + if (options.containsSlice(option :: args)) options + else options ::: option :: args + } + } + override def projectSettings: Seq[Setting[_]] = Seq( Def.derive( scalacOptions := { + val prevOptions = scalacOptions.value.toList + val managedOptions = tpolecatManagedScalacOptions.value + val effectiveOptions = tpolecatEffectiveScalacOptions.value + + val optionsToRemove = managedOptions.diff(effectiveOptions) + val optionsToAdd = effectiveOptions + + val optionsWithRemovals = optionsToRemove.foldLeft(prevOptions)(removeOption(_, _)) + optionsToAdd.foldLeft(optionsWithRemovals)(addOption(_, _)) + } + ), + Def.derive( + tpolecatManagedScalacOptions ++= tpolecatEffectiveScalacOptions.value + ), + Def.derive( + tpolecatEffectiveScalacOptions := { val pluginOptions = tpolecatScalacOptions.value val pluginExcludes = tpolecatExcludeOptions.value val selectedOptions = pluginOptions.diff(pluginExcludes) - scalacOptionsFor(scalaVersion.value, selectedOptions) + supportedOptionsFor(scalaVersion.value, selectedOptions) } ), Def.derive( @@ -149,6 +209,7 @@ object TpolecatPlugin extends AutoPlugin { ) override def globalSettings: Seq[Def.Setting[_]] = Seq( - tpolecatExcludeOptions := Set.empty + tpolecatManagedScalacOptions := Set.empty, + tpolecatExcludeOptions := Set.empty ) } diff --git a/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/build.sbt b/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/build.sbt index 71e80e3..346f098 100644 --- a/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/build.sbt +++ b/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/build.sbt @@ -8,6 +8,8 @@ val Scala213 = "2.13.8" val Scala30 = "3.0.2" val Scala31 = "3.1.3" +enablePlugins(OtherPlugin) + crossScalaVersions := Seq( Scala211, Scala212, @@ -280,3 +282,30 @@ TaskKey[Unit]("checkThisProjectScalacOptions") := { val options = (Compile / scalacOptions).value assert(options.contains("non-existent-key"), "Scope ThisProject was ignored") } + +addCommandAlias( + "addOtherPluginsScalacOptions", + "set ThisProject / otherPluginActivate := true" +) + +TaskKey[Unit]("checkOtherPluginsScalacOptions") := { + val optionsProject = scalacOptions.value + assert( + optionsProject.contains("other-plugin-option-1"), + "Project scope of OtherPlugin was ignored in Project" + ) + assert( + !optionsProject.contains("other-plugin-option-2"), + "Unexpected Compile-only setting in Project" + ) + + val optionsCompile = (Compile / scalacOptions).value + assert( + optionsCompile.contains("other-plugin-option-1"), + "Project scope of OtherPlugin was ignored in Compile" + ) + assert( + optionsCompile.contains("other-plugin-option-2"), + "Compile scope of OtherPlugin was ignored in Compile" + ) +} diff --git a/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/project/OtherPlugin.scala b/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/project/OtherPlugin.scala new file mode 100644 index 0000000..21cd35e --- /dev/null +++ b/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/project/OtherPlugin.scala @@ -0,0 +1,27 @@ +package build + +import sbt.Keys._ +import sbt._ + +object OtherPlugin extends AutoPlugin { + object autoImport { + val otherPluginActivate = settingKey[Boolean]("activate the options of OtherPlugin") + } + + import autoImport._ + + override def globalSettings: Seq[Setting[_]] = Seq( + otherPluginActivate := false + ) + + override def projectSettings: Seq[Setting[_]] = Seq( + scalacOptions ++= { + if (otherPluginActivate.value) Seq("other-plugin-option-1") + else Nil + }, + Compile / scalacOptions ++= { + if (otherPluginActivate.value) Seq("other-plugin-option-2") + else Nil + } + ) +} diff --git a/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/test b/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/test index 51d99b1..3cc843f 100644 --- a/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/test +++ b/plugin/src/sbt-test/sbt-tpolecat/scalacOptions/test @@ -18,3 +18,6 @@ # Check user can still append their own scalacOptions > addScalacOptionsToThisProject > +checkThisProjectScalacOptions +# Check that other plugins can still append their own scalacOptions +> addOtherPluginsScalacOptions +> +checkOtherPluginsScalacOptions