diff --git a/libraries/stdlib/js/src/kotlin/throwableExtensions.kt b/libraries/stdlib/js/src/kotlin/throwableExtensions.kt index be95aa85586..d73fa9cbf7d 100644 --- a/libraries/stdlib/js/src/kotlin/throwableExtensions.kt +++ b/libraries/stdlib/js/src/kotlin/throwableExtensions.kt @@ -15,7 +15,7 @@ package kotlin * - the detailed description of each throwable in the [Throwable.cause] chain. */ @SinceKotlin("1.4") -public actual fun Throwable.toStringWithTrace(): String = buildString { dumpStackTraceTo("", "", this) } +public actual fun Throwable.toStringWithTrace(): String = buildString { dumpStackTraceTo("", "", this, arrayOf()) } /** * Adds the specified exception to the list of exceptions that were @@ -43,19 +43,27 @@ public actual val Throwable.suppressedExceptions: List } -private fun Throwable.dumpStackTraceTo(indent: String, qualifier: String, target: StringBuilder) { - this.dumpSelfTrace(indent, qualifier, target) +private fun Array.hasSeen(exception: Throwable): Boolean = any { it === exception } + +private fun Throwable.dumpStackTraceTo(indent: String, qualifier: String, target: StringBuilder, visited: Array) { + this.dumpSelfTrace(indent, qualifier, target, visited) || return var cause = this.cause while (cause != null) { // TODO: should skip common stack frames - cause.dumpSelfTrace(indent, "Caused by: ", target) + cause.dumpSelfTrace(indent, "Caused by: ", target, visited) cause = cause.cause } } -private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, target: StringBuilder) { +private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, target: StringBuilder, visited: Array): Boolean { target.append(indent).append(qualifier) + if (visited.hasSeen(this)) { + target.append("[CIRCULAR REFERENCE, SEE ABOVE: ").append(this).append("]\n") + return false + } + visited.asDynamic().push(this) + val stack = this.asDynamic().stack as String? if (stack != null) { if (indent.isNotEmpty()) { @@ -76,7 +84,8 @@ private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, target: S if (suppressed.isNotEmpty()) { val suppressedIndent = indent + '\t' for (s in suppressed) { - s.dumpStackTraceTo(suppressedIndent, "Suppressed: ", target) + s.dumpStackTraceTo(suppressedIndent, "Suppressed: ", target, visited) } } + return true } diff --git a/libraries/stdlib/test/ExceptionTest.kt b/libraries/stdlib/test/ExceptionTest.kt index 9a2f768b37c..e93e89b9b02 100644 --- a/libraries/stdlib/test/ExceptionTest.kt +++ b/libraries/stdlib/test/ExceptionTest.kt @@ -127,6 +127,32 @@ class ExceptionTest { assertInTrace(causeSuppressed) } +// fail(topLevelTrace) // to dump the entire trace + } + + @Test + fun circularSuppressedDetailedTrace() { + if (!supportsSuppressedExceptions) return + + // Testing an exception of the following structure + // e1 + // -- suppressed: e3 + // -- suppressed: e1 + // Caused by: e2 + // -- suppressed: e1 + // Caused by: e3 + + val e3 = Exception("e3") + val e2 = Error("e2", e3) + val e1 = RuntimeException("e1", e2) + e1.addSuppressed(e3) + e3.addSuppressed(e1) + e2.addSuppressed(e1) + + val topLevelTrace = e1.toStringWithTrace() + assertEquals(3, Regex.fromLiteral(e1.toString()).findAll(topLevelTrace).count(), topLevelTrace) + assertEquals(1, Regex.fromLiteral(e2.toString()).findAll(topLevelTrace).count(), topLevelTrace) + assertEquals(2, Regex.fromLiteral(e3.toString()).findAll(topLevelTrace).count(), topLevelTrace) // fail(topLevelTrace) // to dump the entire trace }