diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala b/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala index 9d0b0d7b6..5145718c2 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/StackTrace.scala @@ -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 } diff --git a/internal/util-logging/src/test/scala/sbt/internal/util/StackTraceSpec.scala b/internal/util-logging/src/test/scala/sbt/internal/util/StackTraceSpec.scala new file mode 100644 index 000000000..af8a736b9 --- /dev/null +++ b/internal/util-logging/src/test/scala/sbt/internal/util/StackTraceSpec.scala @@ -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 +}