Improve StateTransform

The StateTransform class introduced in
9cdeb7120e did not cleanly integrate with
logic for transforming the state using the `transformState` task
attribute. For one thing, the state transform was only applied if the
root node returned StateTransform but no transformation would occur if a
task had a dependency that returned StateTransform. Moreover, the
transformation would override all other transformations that may have
occurred during task evaluation.

This commit updates StateTransform to act very similarly to the
transformState attribute. Instead of wrapping a `State` instance, it now
wraps a transformation function from `State => State`. This function
can be applied on top of or prior to the other transformations via the
`transformState`.

For binary compatibility with 1.3.0, I had to add the stateProxy
function as a constructor parameter in order to implement the `state`
method. The proxy function will generally throw an exception unless the
legacy api is used. To avoid that, I private[sbt]'d the legacy api so
that binary compatibility is preserved but any builds targeting > 1.4.x
will be required to use the new api.

Unfortunately I couldn't private[sbt] StateTransform.apply(state: State)
because mima interpreted it as a method type change becuase I added
StateTransform.apply(transform: State => State). This may be a mima bug
given that StateTransform.apply(state: State) would be jvm public even
when private[sbt], but I figured it was quite unlikely that any users
were using this method anyway since it was incorrectly implemented in
1.3.0 to return `state` instead of `new StateTransform(state)`.
This commit is contained in:
Ethan Atkins 2019-11-17 09:16:29 -08:00
parent 181bfe8a46
commit a83c280db1
8 changed files with 93 additions and 22 deletions

View File

@ -515,17 +515,14 @@ object EvaluateTask {
state: State,
root: Task[T]
): (State, Result[T]) = {
val newState = results(root) match {
case Value(KeyValue(_, st: StateTransform) :: Nil) => st.state
case _ => stateTransform(results)(state)
}
(newState, results(root))
(stateTransform(results)(state), results(root))
}
def stateTransform(results: RMap[Task, Result]): State => State =
Function.chain(
results.toTypedSeq flatMap {
case results.TPair(Task(info, _), Value(v)) => info.post(v) get transformState
case _ => Nil
case results.TPair(_, Value(KeyValue(_, st: StateTransform))) => Some(st.transform)
case results.TPair(Task(info, _), Value(v)) => info.post(v) get transformState
case _ => Nil
}
)

View File

@ -7,14 +7,43 @@
package sbt
final class StateTransform(val state: State) {
override def equals(o: Any): Boolean = o match {
case that: StateTransform => this.state == that.state
case _ => false
}
override def hashCode: Int = state.hashCode
override def toString: String = s"StateTransform($state)"
/**
* Provides a mechanism for a task to transform the state based on the result of the task. For
* example:
* {{{
* val foo = AttributeKey[String]("foo")
* val setFoo = taskKey[StateTransform]("")
* setFoo := {
* state.value.get(foo) match {
* case None => StateTransform(_.put(foo, "foo"))
* case _ => StateTransform(identity)
* }
* }
* val getFoo = taskKey[Option[String]]
* getFoo := state.value.get(foo)
* }}}
* Prior to a call to `setFoo`, `getFoo` will return `None`. After a call to `setFoo`, `getFoo` will
* return `Some("foo")`.
*/
final class StateTransform private (val transform: State => State, stateProxy: () => State) {
@deprecated("Exists only for binary compatibility with 1.3.x.", "1.4.0")
private[sbt] def state: State = stateProxy()
@deprecated("1.4.0", "Use the constructor that takes a transform function.")
private[sbt] def this(state: State) = this((_: State) => state, () => state)
}
object StateTransform {
@deprecated("Exists only for binary compatibility with 1.3.x", "1.4.0")
def apply(state: State): State = state
/**
* Create an instance of [[StateTransform]].
* @param transform the transformation to apply after task evaluation has completed
* @return the [[StateTransform]].
*/
def apply(transform: State => State) =
new StateTransform(
transform,
() => throw new IllegalStateException("No state was added to the StateTransform.")
)
}

View File

@ -117,7 +117,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
private[sbt] def continuousTask: Def.Initialize[InputTask[StateTransform]] =
Def.inputTask {
val (initialCount, commands) = continuousParser.parsed
new StateTransform(runToTermination(state.value, commands, initialCount, isCommand = false))
val newState = runToTermination(state.value, commands, initialCount, isCommand = false)
StateTransform(_ => newState)
}
private[sbt] val dynamicInputs = taskKey[Option[mutable.Set[DynamicInput]]](

View File

@ -24,7 +24,7 @@ private[sbt] object CheckBuildSources {
val st: State = state.value
val firstTime = st.get(hasCheckedMetaBuild).fold(true)(_.compareAndSet(false, true))
(onChangedBuildSource in Scope.Global).value match {
case IgnoreSourceChanges => new StateTransform(st)
case IgnoreSourceChanges => StateTransform(identity)
case o =>
import sbt.nio.FileStamp.Formats._
logger.debug("Checking for meta build source updates")
@ -47,16 +47,16 @@ private[sbt] object CheckBuildSources {
logger.info(s"$prefix\nReloading sbt...")
val remaining =
Exec("reload", None, None) :: st.currentCommand.toList ::: st.remainingCommands
new StateTransform(st.copy(currentCommand = None, remainingCommands = remaining))
StateTransform(_.copy(currentCommand = None, remainingCommands = remaining))
} else {
val tail = "Apply these changes by running `reload`.\nAutomatically reload the " +
"build when source changes are detected by setting " +
"`Global / onChangedBuildSource := ReloadOnSourceChanges`.\nDisable this " +
"warning by setting `Global / onChangedBuildSource := IgnoreSourceChanges`."
logger.warn(s"$prefix\n$tail")
new StateTransform(st)
StateTransform(identity)
}
case _ => new StateTransform(st)
case _ => StateTransform(identity)
}
}
}

View File

@ -0,0 +1,31 @@
import complete.DefaultParsers._
import complete.Parser
import sbt.Def.Initialize
val keep = taskKey[Int]("")
val checkKeep = inputKey[Unit]("")
keep := (getPrevious(keep) map { case None => 3; case Some(x) => x + 1 } keepAs keep).value
checkKeep := inputCheck((ctx, s) => Space ~> str(getFromContext(keep, ctx, s))).evaluated
val foo = AttributeKey[Int]("foo")
val transform = taskKey[StateTransform]("")
transform := {
keep.value
StateTransform(_.put(foo, 1))
}
def inputCheck[T](f: (ScopedKey[_], State) => Parser[T]): Initialize[InputTask[Unit]] =
InputTask(resolvedScoped(ctx => (s: State) => f(ctx, s)))(dummyTask)
val checkTransform = taskKey[Unit]("")
checkTransform := {
assert(state.value.get(foo).contains(1))
}
def dummyTask: Any => Initialize[Task[Unit]] =
(_: Any) =>
maxErrors map { _ =>
()
}
def str(o: Option[Int]) = o match { case None => "blue"; case Some(i) => i.toString }

View File

@ -0,0 +1,13 @@
> checkKeep blue
-> checkTransform
> transform
> checkKeep 3
> checkTransform
> transform
> checkKeep 4

View File

@ -38,8 +38,8 @@ object Build {
checkStringValue := checkStringValueImpl.evaluated,
watchOnFileInputEvent := { (_, _) => Watch.CancelWatch },
watchTasks := Def.inputTask {
val prev = watchTasks.evaluated
new StateTransform(prev.state.fail)
watchTasks.evaluated
StateTransform(_.fail)
}.evaluated
)
}

View File

@ -25,7 +25,7 @@ object Build {
watchOnFileInputEvent := { (_, _) => Watch.CancelWatch },
watchTasks := Def.inputTask {
val prev = watchTasks.evaluated
new StateTransform(prev.state.fail)
StateTransform(_.fail)
}.evaluated
)
}