Skip to content

Commit

Permalink
Fix #102: Do not overwrite other plugins' scalacOptions.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sjrd committed Oct 28, 2022
1 parent 15b090c commit 063ff5e
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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](
Expand Down Expand Up @@ -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._
Expand Down Expand Up @@ -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(
Expand All @@ -149,6 +209,7 @@ object TpolecatPlugin extends AutoPlugin {
)

override def globalSettings: Seq[Def.Setting[_]] = Seq(
tpolecatExcludeOptions := Set.empty
tpolecatManagedScalacOptions := Set.empty,
tpolecatExcludeOptions := Set.empty
)
}
29 changes: 29 additions & 0 deletions plugin/src/sbt-test/sbt-tpolecat/scalacOptions/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
)
}
Original file line number Diff line number Diff line change
@@ -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
}
)
}
3 changes: 3 additions & 0 deletions plugin/src/sbt-test/sbt-tpolecat/scalacOptions/test
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 063ff5e

Please sign in to comment.