diff --git a/main-settings/src/main/scala/sbt/Def.scala b/main-settings/src/main/scala/sbt/Def.scala index c75d19960..6a49cd985 100644 --- a/main-settings/src/main/scala/sbt/Def.scala +++ b/main-settings/src/main/scala/sbt/Def.scala @@ -404,8 +404,16 @@ object Def extends BuildSyntax with Init with InitializeImplicits: extension [A1](inline in: TaskKey[A1]) // implicit def macroPrevious[T](@deprecated("unused", "") in: TaskKey[T]): MacroPrevious[T] = ??? - inline def previous(using JsonFormat[A1]): Option[A1] = - InputWrapper.`wrapInitTask_\u2603\u2603`[Option[A1]](Previous.runtime[A1](in)) + // Previously, we implemented `.previous` as a plain inline expansion of + // `wrapInitTask(Previous.runtime(in))`. For composite types such as `Seq[String]`, the + // compiler can rewrite the format argument into a synthetic local like + // `{ val x$2$proxy1 = immSeqFormat(StringJsonFormat); Previous.runtime(in)(using x$2$proxy1) }`. + // The outer task macro later hoists only `Previous.runtime(...)` as an input dependency, + // leaving `x$2$proxy1` behind and causing "used outside the scope where it was defined". + // Now we implement `.previous` as a macro so the wrapped tree is already self-contained. + // See https://github.com/sbt/sbt/issues/9037. + inline def previous(using @scala.annotation.unused format: JsonFormat[A1]): Option[A1] = + ${ TaskMacro.previousImpl[A1]('in) } // The following conversions enable the types Parser[T], Initialize[Parser[T]], and // Initialize[State => Parser[T]] to be used in the inputTask macro as an input with an ultimate diff --git a/main-settings/src/main/scala/sbt/std/TaskMacro.scala b/main-settings/src/main/scala/sbt/std/TaskMacro.scala index ee10da330..be91e3cfe 100644 --- a/main-settings/src/main/scala/sbt/std/TaskMacro.scala +++ b/main-settings/src/main/scala/sbt/std/TaskMacro.scala @@ -17,6 +17,7 @@ import sbt.internal.util.{ SourcePosition, SourcePositionImpl } import language.experimental.macros import scala.quoted.* import sbt.util.BuildWideCacheConfiguration +import sjsonnew.JsonFormat object TaskMacro: final val AssignInitName = "set" @@ -120,6 +121,15 @@ object TaskMacro: val convert1 = new FullConvert(qctx, 1000) convert1.contFlatMap[A1, F, Id](t, convert1.appExpr, None) + def previousImpl[A1: Type](t: Expr[TaskKey[A1]])(using qctx: Quotes): Expr[Option[A1]] = + import qctx.reflect.* + Expr.summon[JsonFormat[A1]] match + case Some(ev) => + '{ + InputWrapper.`wrapInitTask_\u2603\u2603`[Option[A1]](Previous.runtime[A1]($t)(using $ev)) + } + case _ => report.errorAndAbort(s"JsonFormat[${Type.show[A1]}] missing") + /** Implementation of := macro for settings. */ def settingAssignMacroImpl[A1: Type](rec: Expr[Scoped.DefinableSetting[A1]], v: Expr[A1])(using qctx: Quotes diff --git a/main-settings/src/test/scala/sbt/std/TaskPosSpec.scala b/main-settings/src/test/scala/sbt/std/TaskPosSpec.scala index da3a22327..c82916515 100644 --- a/main-settings/src/test/scala/sbt/std/TaskPosSpec.scala +++ b/main-settings/src/test/scala/sbt/std/TaskPosSpec.scala @@ -106,6 +106,23 @@ class TaskPosSpec { } } + locally { + // .previous should compile for task with complex return type like `Seq[String]` + // https://github.com/sbt/sbt/issues/9037 + import sbt.*, Def.* + import sjsonnew.BasicJsonProtocol.given + val link = taskKey[Int]("") + val fingerprints = taskKey[Seq[String]]("") + Def.taskDyn[Int] { + val currentFingerprints = (link / fingerprints).value + val previousFingerprints = (link / fingerprints).previous + Def.task { + if previousFingerprints.exists(_ != currentFingerprints) then currentFingerprints.size + else 0 + } + } + } + locally { // missing .value error should not happen inside task dyn import sbt.*, Def.*