Skip to content

Commit aed65c0

Browse files
authored
Add proper error message when circular task dependency is detected (#6044)
This provides a nicer error that is easier to understand than the `StackOverflow` that was reported before
1 parent 6068aee commit aed65c0

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

core/api/src/mill/api/internal/Cacher.scala

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
package mill.api.internal
22

3+
import collection.mutable.LinkedHashSet
34
import scala.collection.mutable
5+
import scala.util.DynamicVariable
46
import scala.quoted.*
57

68
trait Cacher extends mill.moduledefs.Cacher {
79
private lazy val cacherLazyMap = mutable.Map.empty[sourcecode.Enclosing, Any]
810

911
protected def cachedTask[T](t: => T)(using c: sourcecode.Enclosing): T = synchronized {
10-
cacherLazyMap.getOrElseUpdate(c, t).asInstanceOf[T]
12+
if (Cacher.taskEvaluationStack.value.contains((c, this))) {
13+
sys.error(
14+
"Circular task dependency detected:\n" +
15+
(Cacher.taskEvaluationStack.value.toList ++ Seq((c, this)))
16+
.map { case (c, o) =>
17+
val taskName = c.value.split("\\.|#| ").filter(!_.startsWith("$anon")).last
18+
o.toString match {
19+
case "" => taskName
20+
case s => s + "." + taskName
21+
}
22+
}
23+
.mkString("\ndepends on: ")
24+
)
25+
}
26+
27+
try {
28+
Cacher.taskEvaluationStack.value.add((c, this))
29+
cacherLazyMap.getOrElseUpdate(c, t).asInstanceOf[T]
30+
} finally {
31+
Cacher.taskEvaluationStack.value.remove((c, this))
32+
}
1133
}
1234
}
1335

1436
private[mill] object Cacher {
37+
// Use a LinkedHashSet for fast contains checking while preserving insertion order
38+
private[mill] val taskEvaluationStack =
39+
DynamicVariable[LinkedHashSet[(sourcecode.Enclosing, Any)]](LinkedHashSet())
1540
private[mill] def withMacroOwner[T](using Quotes)(op: quotes.reflect.Symbol => T): T = {
1641
import quotes.reflect.*
1742
// In Scala 3, the top level splice of a macro is owned by a symbol called "macro" with the macro flag set,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package build
2+
import mill._
3+
4+
object module extends Module {
5+
6+
object nested extends Module {
7+
def taskA: T[String] = Task { taskB() }
8+
}
9+
def taskB: T[String] = Task { taskC() }
10+
}
11+
def taskC: T[String] = Task { module.nested.taskA() }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package mill.integration
2+
3+
import mill.testkit.UtestIntegrationTestSuite
4+
5+
import utest._
6+
7+
object CircularTasksTests extends UtestIntegrationTestSuite {
8+
def captureOutErr = true
9+
val tests: Tests = Tests {
10+
test - integrationTest { tester =>
11+
// Ensure that resolve works even of the modules containing the resolved
12+
// tasks are broken
13+
val res = tester.eval(("module.nested.taskA"))
14+
assert(res.err.contains(
15+
"""Circular task dependency detected:
16+
|module.nested.taskA
17+
|depends on: module.taskB
18+
|depends on: taskC
19+
|depends on: module.nested.taskA""".stripMargin.replace("\r\n", "\n")
20+
))
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)