[2.x] fix: Resolve Seq[Initialize[Task[T]]].join to Initialize[Task[Seq[T]]] (#899)

Fixes #899.

`.join` on a `Seq[Initialize[Task[T]]]` could resolve through the generic
`Initialize.joinInitialize` conversion (yielding `Initialize[Seq[Task[T]]]`)
instead of the task-flattening `Initialize[Task[Seq[T]]]`, whenever the
task-specialized `Scoped.richTaskSeq` was not in scope.

Add the task join as an extension method on `Seq[Def.Initialize[Task[A]]]` in
`object Def`. Member resolution tries extension methods before implicit
conversions, so it is selected ahead of the generic conversion regardless of
whether `sbt.Scoped` is in scope -- and, being in a different resolution phase
than the existing `Scoped.richTaskSeq` conversion, it does not become a second
ambiguous candidate for `TaskKey`/`SettingKey` seqs (where `Scoped` is a base
type). `Scoped.richTaskSeq` is left unchanged as the existing public implicit.

Also:
- Drop the now-unnecessary `import sbt.Scoped.richTaskSeq` from
  `BuildServerProtocol` and simplify `ScopeFilter.all`'s `.join(_.join)` to
  `.join`.
- Make the two `dependsOn(Initialize[? <: Task[?]]*)` overloads delegate to
  `dependsOnSeq`, which uses the explicit `Initialize.joinAny[Task]` raw-task
  form, instead of relying on `.join` resolving to the generic conversion.
- Add a compile-only regression test pinning resolution for both a plain
  `Seq[Initialize[Task[Int]]]` and a `Seq[TaskKey[Int]]`, inferring the result
  with no expected type.
This commit is contained in:
Jozef Koval 2026-06-16 00:43:40 +02:00
parent 183ed46375
commit 4193607fa3
5 changed files with 44 additions and 9 deletions

View File

@ -495,4 +495,15 @@ sealed trait InitializeImplicits { self: Def.type =>
implicit def initTaskOps[T](x: Def.Initialize[Task[T]]): Def.InitTaskOps[T] =
new Def.InitTaskOps(x)
/**
* Flattens a sequence of task initializations into one task producing the sequence of results.
*
* This is an extension method rather than an implicit conversion so that it is selected ahead of
* the generic `Initialize.joinInitialize` conversion (which would otherwise yield
* `Initialize[Seq[Task[A]]]`) regardless of whether `sbt.Scoped` is in scope, and without becoming
* a second, ambiguous conversion candidate for `TaskKey`/`SettingKey` seqs. See issue #899.
*/
extension [A](in: Seq[Def.Initialize[Task[A]]])
def join: Def.Initialize[Task[Seq[A]]] = Scoped.richTaskSeq(in).join
}

View File

@ -364,7 +364,7 @@ object Scoped:
// Task-specific extensions
def dependsOn(tasks: Initialize[? <: Task[?]]*): Initialize[Task[A1]] =
init.zipWith(tasks.asInstanceOf[Seq[Initialize[Task[?]]]].join)(_.dependsOn(_*))
dependsOnSeq(tasks.asInstanceOf[Seq[AnyInitTask]])
def dependsOnTask[A2](task1: Initialize[Task[A2]]): Initialize[Task[A1]] =
dependsOnSeq(Seq[AnyInitTask](task1.asInstanceOf[AnyInitTask]))
def dependsOnSeq(tasks: Seq[AnyInitTask]): Initialize[Task[A1]] =
@ -412,9 +412,7 @@ object Scoped:
// InputTask specific extensions
@targetName("dependsOnInitializeInputTask")
def dependsOn(tasks: Initialize[? <: Task[?]]*): Initialize[InputTask[A1]] =
init.zipWith(tasks.asInstanceOf[Seq[Initialize[Task[?]]]].join)((thisTask, deps) =>
thisTask.mapTask(_.dependsOn(deps*))
)
dependsOnSeq(tasks.asInstanceOf[Seq[AnyInitTask]])
@targetName("dependsOnTaskInitializeInputTask")
def dependsOnTask[B1](task1: Initialize[Task[B1]]): Initialize[InputTask[A1]] =
dependsOnSeq(Seq[AnyInitTask](task1.asInstanceOf[AnyInitTask]))

View File

@ -0,0 +1,30 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.join
import sbt.{ Def, Task, TaskKey }
object JoinResolutionTest:
// `.join` on a seq of task initializes must resolve to Initialize[Task[Seq[Int]]] (task-flattening),
// not the generic Initialize[Seq[Task[Int]]]. Each result is inferred with NO expected type first
// (`val joined = ...`) and only then checked against the declared return type, so the test fails if
// resolution depends on an expected type to pick the right form. This package does not import
// `sbt.Scoped`, so it pins that the resolution holds via implicit scope alone. Guards issue #899.
// The originally reported shape: a plain `Seq[Initialize[Task[A]]]` (no `Scoped` base type).
def check(in: Seq[Def.Initialize[Task[Int]]]): Def.Initialize[Task[Seq[Int]]] =
val joined = in.join
joined
// `TaskKey` extends `Scoped`, so this also pins that the task join wins without becoming ambiguous
// with the `Scoped.richTaskSeq` conversion that is in implicit scope here.
def checkTaskKey(in: Seq[TaskKey[Int]]): Def.Initialize[Task[Seq[Int]]] =
val joined = in.join
joined
end JoinResolutionTest

View File

@ -117,13 +117,10 @@ object ScopeFilter {
* static inspections will not show them.
*/
def all(sfilter: => ScopeFilter): Initialize[Task[Seq[A]]] = Def.flatMap(getData) { data =>
import std.TaskExtra.*
val filter = sfilter
orderedScopes(filter(data), filter.scopeOrdering(data))
.map(s => Project.inScope(s, i))
.join(
_.join
)
.join
}
private type ScopeAxisOrderingKey = (Int, String)

View File

@ -17,7 +17,6 @@ import sbt.Def.*
import sbt.Keys.*
import sbt.ProjectExtra.*
import sbt.ScopeFilter.Make.*
import sbt.Scoped.richTaskSeq
import sbt.SlashSyntax0.*
import sbt.StandardMain.exchange
import sbt.internal.bsp.*