Skip to content

Commit cd843e6

Browse files
committed
Fix typelevel#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.
1 parent 15b090c commit cd843e6

File tree

4 files changed

+113
-5
lines changed

4 files changed

+113
-5
lines changed

plugin/src/main/scala/io/github/davidgregory084/TpolecatPlugin.scala

+66-5
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ object TpolecatPlugin extends AutoPlugin {
2929
object autoImport {
3030
object ScalacOptions extends ScalacOptions
3131

32-
def scalacOptionsFor(
32+
private[TpolecatPlugin] def supportedOptionsFor(
3333
version: String,
3434
modeScalacOptions: Set[ScalacOption]
35-
): Seq[String] = {
36-
val supportedOptions = (CrossVersion.partialVersion(version), version.split('.')) match {
35+
): Set[ScalacOption] = {
36+
(CrossVersion.partialVersion(version), version.split('.')) match {
3737
case (Some((0, _)), _) => // dotty prereleases use 0 as major version
3838
modeScalacOptions
3939
.filter(_.isSupported(V3_0_0)) // treat dotty prereleases as 3.0.0
@@ -47,8 +47,13 @@ object TpolecatPlugin extends AutoPlugin {
4747
case (None, _) =>
4848
Set.empty[ScalacOption]
4949
}
50+
}
5051

51-
supportedOptions.toList.flatMap(opt => opt.option :: opt.args)
52+
def scalacOptionsFor(
53+
version: String,
54+
modeScalacOptions: Set[ScalacOption]
55+
): Seq[String] = {
56+
supportedOptionsFor(version, modeScalacOptions).toList.flatMap(opt => opt.option :: opt.args)
5257
}
5358

5459
val tpolecatDefaultOptionsMode = settingKey[OptionsMode](
@@ -90,6 +95,14 @@ object TpolecatPlugin extends AutoPlugin {
9095
val tpolecatExcludeOptions = settingKey[Set[ScalacOption]](
9196
"The set of scalac options that will be excluded."
9297
)
98+
99+
val tpolecatEffectiveScalacOptions = settingKey[Set[ScalacOption]](
100+
"The set of scalac options that will effectively be applied by the sbt-tpolecat. For internal use only."
101+
).withRank(sbt.KeyRanks.Invisible)
102+
103+
val tpolecatManagedScalacOptions = settingKey[Set[ScalacOption]](
104+
"The set of scalac options that sbt-tpolecat owns and manages. Defaults to anything it ever adds in any scope delegation chain."
105+
).withRank(sbt.KeyRanks.DSetting)
93106
}
94107

95108
import autoImport._
@@ -122,13 +135,60 @@ object TpolecatPlugin extends AutoPlugin {
122135
tpolecatDevModeOptions := ScalacOptions.default
123136
) ++ commandAliases
124137

138+
private def removeOption(options: List[String], optionToRemove: ScalacOption): List[String] = {
139+
val option = optionToRemove.option
140+
val args = optionToRemove.args
141+
142+
if (args.isEmpty) {
143+
// fast path
144+
options.filterNot(_ == option)
145+
} else {
146+
def loop(options: List[String]): List[String] = options match {
147+
case Nil => Nil
148+
case `option` :: tail if tail.startsWith(args) => loop(tail.drop(args.size))
149+
case head :: tail => head :: loop(tail)
150+
}
151+
loop(options)
152+
}
153+
}
154+
155+
private def addOption(options: List[String], optionToAdd: ScalacOption): List[String] = {
156+
val option = optionToAdd.option
157+
val args = optionToAdd.args
158+
159+
if (args.isEmpty) {
160+
// fast path
161+
if (options.contains(option)) options
162+
else options ::: option :: Nil
163+
} else {
164+
if (options.containsSlice(option :: args)) options
165+
else options ::: option :: args
166+
}
167+
}
168+
125169
override def projectSettings: Seq[Setting[_]] = Seq(
126170
Def.derive(
127171
scalacOptions := {
172+
val prevOptions = scalacOptions.value.toList
173+
val managedOptions = tpolecatManagedScalacOptions.value
174+
val effectiveOptions = tpolecatEffectiveScalacOptions.value
175+
176+
val optionsToRemove = managedOptions.diff(effectiveOptions)
177+
val optionsToAdd = effectiveOptions
178+
179+
val optionsWithRemovals = optionsToRemove.foldLeft(prevOptions)(removeOption(_, _))
180+
optionsToAdd.foldLeft(optionsWithRemovals)(addOption(_, _))
181+
}
182+
),
183+
Def.derive(
184+
tpolecatManagedScalacOptions ++= tpolecatEffectiveScalacOptions.value
185+
),
186+
Def.derive(
187+
tpolecatEffectiveScalacOptions := {
128188
val pluginOptions = tpolecatScalacOptions.value
129189
val pluginExcludes = tpolecatExcludeOptions.value
130190
val selectedOptions = pluginOptions.diff(pluginExcludes)
131-
scalacOptionsFor(scalaVersion.value, selectedOptions)
191+
supportedOptionsFor(scalaVersion.value, selectedOptions)
132192
}
133193
),
134194
Def.derive(
@@ -149,6 +209,7 @@ object TpolecatPlugin extends AutoPlugin {
149209
)
150210

151211
override def globalSettings: Seq[Def.Setting[_]] = Seq(
212+
tpolecatManagedScalacOptions := Set.empty,
152213
tpolecatExcludeOptions := Set.empty
153214
)
154215
}

plugin/src/sbt-test/sbt-tpolecat/scalacOptions/build.sbt

+17
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ val Scala213 = "2.13.8"
88
val Scala30 = "3.0.2"
99
val Scala31 = "3.1.3"
1010

11+
enablePlugins(OtherPlugin)
12+
1113
crossScalaVersions := Seq(
1214
Scala211,
1315
Scala212,
@@ -280,3 +282,18 @@ TaskKey[Unit]("checkThisProjectScalacOptions") := {
280282
val options = (Compile / scalacOptions).value
281283
assert(options.contains("non-existent-key"), "Scope ThisProject was ignored")
282284
}
285+
286+
addCommandAlias(
287+
"addOtherPluginsScalacOptions",
288+
"set ThisProject / otherPluginActivate := true"
289+
)
290+
291+
TaskKey[Unit]("checkOtherPluginsScalacOptions") := {
292+
val optionsProject = scalacOptions.value
293+
assert(optionsProject.contains("other-plugin-option-1"), "Project scope of OtherPlugin was ignored in Project")
294+
assert(!optionsProject.contains("other-plugin-option-2"), "Unexpected Compile-only setting in Project")
295+
296+
val optionsCompile = (Compile / scalacOptions).value
297+
assert(optionsCompile.contains("other-plugin-option-1"), "Project scope of OtherPlugin was ignored in Compile")
298+
assert(optionsCompile.contains("other-plugin-option-2"), "Compile scope of OtherPlugin was ignored in Compile")
299+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package build
2+
3+
import sbt.Keys._
4+
import sbt._
5+
6+
object OtherPlugin extends AutoPlugin {
7+
object autoImport {
8+
val otherPluginActivate = settingKey[Boolean]("activate the options of OtherPlugin")
9+
}
10+
11+
import autoImport._
12+
13+
override def globalSettings: Seq[Setting[_]] = Seq(
14+
otherPluginActivate := false
15+
)
16+
17+
override def projectSettings: Seq[Setting[_]] = Seq(
18+
scalacOptions ++= {
19+
if (otherPluginActivate.value) Seq("other-plugin-option-1")
20+
else Nil
21+
},
22+
Compile / scalacOptions ++= {
23+
if (otherPluginActivate.value) Seq("other-plugin-option-2")
24+
else Nil
25+
}
26+
)
27+
}

plugin/src/sbt-test/sbt-tpolecat/scalacOptions/test

+3
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@
1818
# Check user can still append their own scalacOptions
1919
> addScalacOptionsToThisProject
2020
> +checkThisProjectScalacOptions
21+
# Check that other plugins can still append their own scalacOptions
22+
> addOtherPluginsScalacOptions
23+
> +checkOtherPluginsScalacOptions

0 commit comments

Comments
 (0)