mirror of
https://github.com/sbt/sbt.git
synced 2026-10-06 10:03:56 +02:00
[2.x] fix: Warn on output declarations in uncached tasks (#9770)
This commit is contained in:
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user