mirror of https://github.com/sbt/sbt.git
Fix StackOverflowError when reporting self-referencing exceptions (#8508)
Add circular reference detection to StackTrace.trimmedLines using an IdentityHashMap-backed Set, similar to how the JDK handles this in Throwable.printStackTrace(). When a circular reference is detected, the method now appends a [CIRCULAR REFERENCE: ...] message instead of recursing infinitely. Fixes #7509
This commit is contained in:
parent
f2a5ae7219
commit
02dcab80b9
|
|
@ -10,6 +10,7 @@ package sbt.internal.util
|
|||
|
||||
import sbt.io.IO
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import java.util.{ IdentityHashMap, Collections }
|
||||
|
||||
object StackTrace {
|
||||
def isSbtClass(name: String) = name.startsWith("sbt.") || name.startsWith("xsbt.")
|
||||
|
|
@ -30,6 +31,8 @@ object StackTrace {
|
|||
def trimmedLines(t: Throwable, d: Int): List[String] = {
|
||||
require(d >= 0)
|
||||
val b = new ListBuffer[String]()
|
||||
val seen: java.util.Set[Throwable] =
|
||||
Collections.newSetFromMap(new IdentityHashMap[Throwable, java.lang.Boolean]())
|
||||
|
||||
def appendStackTrace(t: Throwable, first: Boolean): Unit = {
|
||||
|
||||
|
|
@ -58,11 +61,16 @@ object StackTrace {
|
|||
}
|
||||
|
||||
appendStackTrace(t, true)
|
||||
seen.add(t)
|
||||
var c = t
|
||||
while (c.getCause() != null) {
|
||||
while (c.getCause() != null && !seen.contains(c.getCause())) {
|
||||
c = c.getCause()
|
||||
seen.add(c)
|
||||
appendStackTrace(c, false)
|
||||
}
|
||||
if (c.getCause() != null && seen.contains(c.getCause())) {
|
||||
b.append("[CIRCULAR REFERENCE: " + c.getCause().toString + "]")
|
||||
}
|
||||
b.toList
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.util
|
||||
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
|
||||
class StackTraceSpec extends AnyFlatSpec {
|
||||
"StackTrace.trimmedLines" should "handle normal exceptions" in {
|
||||
val exception = new RuntimeException("test exception")
|
||||
val lines = StackTrace.trimmedLines(exception, 3)
|
||||
assert(lines.nonEmpty)
|
||||
assert(lines.head.contains("test exception"))
|
||||
}
|
||||
|
||||
it should "handle exceptions with causes" in {
|
||||
val cause = new RuntimeException("cause exception")
|
||||
val exception = new RuntimeException("test exception", cause)
|
||||
val lines = StackTrace.trimmedLines(exception, 3)
|
||||
assert(lines.exists(_.contains("test exception")))
|
||||
assert(lines.exists(_.contains("Caused by:")))
|
||||
assert(lines.exists(_.contains("cause exception")))
|
||||
}
|
||||
|
||||
it should "handle self-referencing exceptions without StackOverflowError" in {
|
||||
val exception = new SelfReferencingException("self-referencing exception")
|
||||
val lines = StackTrace.trimmedLines(exception, 3)
|
||||
assert(lines.nonEmpty)
|
||||
assert(lines.head.contains("self-referencing exception"))
|
||||
assert(lines.exists(_.contains("[CIRCULAR REFERENCE:")))
|
||||
}
|
||||
|
||||
it should "handle circular exception chains without StackOverflowError" in {
|
||||
val exception1 = new ChainableException("exception 1")
|
||||
val exception2 = new ChainableException("exception 2")
|
||||
exception1.setCauseException(exception2)
|
||||
exception2.setCauseException(exception1)
|
||||
val lines = StackTrace.trimmedLines(exception1, 3)
|
||||
assert(lines.nonEmpty)
|
||||
assert(lines.exists(_.contains("exception 1")))
|
||||
assert(lines.exists(_.contains("exception 2")))
|
||||
assert(lines.exists(_.contains("[CIRCULAR REFERENCE:")))
|
||||
}
|
||||
}
|
||||
|
||||
class SelfReferencingException(message: String) extends RuntimeException(message) {
|
||||
override def getCause: Throwable = this
|
||||
}
|
||||
|
||||
class ChainableException(message: String) extends RuntimeException(message) {
|
||||
import scala.compiletime.uninitialized
|
||||
private var causeException: Throwable = uninitialized
|
||||
def setCauseException(cause: Throwable): Unit = causeException = cause
|
||||
override def getCause: Throwable = causeException
|
||||
}
|
||||
Loading…
Reference in New Issue