[2.x] fix: Warn on output declarations in uncached tasks (#9770)

This commit is contained in:
Anatolii Kmetiuk
2026-09-15 23:30:59 -04:00
committed by GitHub
parent 3460089ec7
commit 152c66a34c
4 changed files with 180 additions and 2 deletions
@@ -508,7 +508,7 @@ trait Cont:
var nowarnQuals: Set[Term] = Set.empty
// Called when transforming the tree to add an input.
// For `qual` of type F[A], and a `selection` qual.value.
val record = [a] =>
lazy val record: [a] => (String, Type[a], Term, Term) => Converted = [a] =>
(name: String, tpe: Type[a], qual: Term, oldTree: Term) =>
given t: Type[a] = tpe
convert[a](name, qual) transform { (replacement: Term) =>
@@ -527,7 +527,13 @@ trait Cont:
$outputAccRef,
)
}.asTerm
else oldTree
else
report.warning(
"Def.declareOutput has no caching effect in an uncached task.",
qual.underlyingArgument.pos,
)
transformWrappers(qual, record, Symbol.spliceOwner)
end if
case WrapOutputDirectoryName =>
val output = Output(
// even though the term is VirtualFileRef, we want the output to make VirtualFile,
@@ -0,0 +1,96 @@
/*
* sbt
* Copyright 2026, Scala center
* Licensed under Apache License 2.0 (see LICENSE)
*/
import sbt.internal.util.StringVirtualFile1
import xsbti.VirtualFile
@transient lazy val zero = taskKey[Unit]("")
@transient lazy val one = taskKey[Unit]("")
@transient lazy val many = taskKey[Unit]("")
@transient lazy val nested = taskKey[Unit]("")
@transient lazy val direct = taskKey[VirtualFile]("")
@transient lazy val viaTask = taskKey[Unit]("")
@transient lazy val viaUncachedTask = taskKey[Unit]("")
@transient lazy val viaDynamic = taskKey[Unit]("")
@transient lazy val checkRuns = taskKey[Unit]("")
lazy val optedOut = taskKey[Unit]("")
Global / localCacheDirectory := baseDirectory.value / "diskcache"
zero := {
val output = StringVirtualFile1("zero.txt", "zero")
var calls = 0
val result = Def.declareOutput {
calls += 1
output
}
assert(result eq output)
assert(calls == 1)
if false then Def.declareOutput { calls += 1; output }
assert(calls == 1)
val results = List(1, 2, 3).map: _ =>
Def.declareOutput { calls += 1; output }
assert(results.forall(_ eq output))
assert(calls == 4)
val failure = new RuntimeException("argument failure")
val thrown = try
Def.declareOutput(throw failure)
None
catch case e: RuntimeException => Some(e)
assert(thrown.contains(failure))
}
one := {
val dir = target.value
val output = StringVirtualFile1("one.txt", "one")
val result = Def.declareOutput(output)
assert(result eq output)
assert(dir.getName.nonEmpty)
}
many := {
val converter = fileConverter.value
val dir = target.value
val output = converter.toVirtualFile((dir / "many.txt").toPath)
assert(Def.declareOutput(output) eq output)
val counter = dir / "runs"
val previous = if counter.exists then IO.read(counter).toInt else 0
IO.write(counter, (previous + 1).toString)
}
direct := StringVirtualFile1("direct.txt", "direct")
nested := {
val result = Def.declareOutput(direct.value)
assert(result.id == "direct.txt")
val converted = Def.declareOutput(fileConverter.value.toVirtualFile(target.value.toPath))
assert(converted.id.nonEmpty)
val output = StringVirtualFile1("nested.txt", "nested")
assert(Def.declareOutput(Def.declareOutput(output)) eq output)
}
optedOut := Def.uncached {
val output = StringVirtualFile1("opted-out.txt", "opted-out")
assert(Def.declareOutput(output) eq output)
}
viaTask := Def.task {
val output = StringVirtualFile1("task.txt", "task")
assert(Def.declareOutput(output) eq output)
}.value
viaUncachedTask := Def.uncachedTask {
val output = StringVirtualFile1("uncached-task.txt", "uncached-task")
assert(Def.declareOutput(output) eq output)
}.value
viaDynamic := Def.taskDyn {
val output = StringVirtualFile1("dynamic.txt", "dynamic")
assert(Def.declareOutput(output) eq output)
Def.task(())
}.value
checkRuns := assert(IO.read(target.value / "runs") == "2")
+10
View File
@@ -0,0 +1,10 @@
> zero
> one
> many
> many
> checkRuns
> nested
> optedOut
> viaTask
> viaUncachedTask
> viaDynamic
@@ -0,0 +1,66 @@
/*
* sbt
* Copyright 2026, Scala center
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.interfaces.Diagnostic.WARNING
import dotty.tools.dotc.reporting.Diagnostic
import hedgehog.Result
import hedgehog.runner.*
import java.nio.file.{ Path, Paths }
import sbt.internal.{ Eval, EvalImports, EvalReporter }
import scala.io.Source
import scala.util.Using
object UncachedOutputWarningSpec extends Properties:
private val warning = "Def.declareOutput has no caching effect in an uncached task."
private val imports = EvalImports(Seq("import sbt.*", "import sbt.given", "import sbt.Keys.*"))
private lazy val classpath: Seq[Path] =
val resource = Option(getClass.getResourceAsStream("/sbt-eval-classpath.txt"))
.getOrElse(sys.error("Missing generated sbt evaluator classpath"))
Using.resource(Source.fromInputStream(resource, "UTF-8")):
_.getLines().map(Paths.get(_)).toVector
private val body = """{
| val output = sbt.internal.util.StringVirtualFile1("output.txt", "content")
| Def.declareOutput(output)
| ()
|}""".stripMargin
override def tests: List[Test] = List(
("uncached task", s"Def.uncachedTask $body", Vector(WARNING -> warning)),
(
"transient task",
s"object Build { @transient lazy val task = taskKey[Unit](\"\"); val setting = task := $body }; Build.setting",
Vector(WARNING -> warning)
),
(
"opted-out task",
s"val task = taskKey[Unit](\"\"); task := Def.uncached $body",
Vector(WARNING -> warning)
),
("cached task", s"Def.cachedTask $body", Vector.empty),
("uncached task without an output declaration", "Def.uncachedTask(())", Vector.empty),
).map: (name, code, expected) =>
example(
name, {
val reporter = new RecordingReporter
val evaluator =
new Eval(Nil, classpath, backingDir = None, mkReporter = Some(() => reporter))
evaluator.eval(code, imports, None, "uncached-output-build.sbt", 1)
Result.assert(reporter.diagnostics == expected).log(reporter.diagnostics.mkString("\n"))
}
)
private final class RecordingReporter extends EvalReporter:
var diagnostics = Vector.empty[(Int, String)]
override def doReport(diagnostic: Diagnostic)(using Context): Unit =
diagnostics :+= diagnostic.level -> diagnostic.msg.message
override def finalReport(sourceName: String): Unit = ()
end UncachedOutputWarningSpec