Prevent infinite recursion in case of circular suppressed chains

#KT-37603
This commit is contained in:
Ilya Gorbunov
2020-03-19 05:46:47 +03:00
parent 552bcdb31b
commit 5fe8071d71
2 changed files with 41 additions and 6 deletions
@@ -15,7 +15,7 @@ package kotlin
* - the detailed description of each throwable in the [Throwable.cause] chain. * - the detailed description of each throwable in the [Throwable.cause] chain.
*/ */
@SinceKotlin("1.4") @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 * Adds the specified exception to the list of exceptions that were
@@ -43,19 +43,27 @@ public actual val Throwable.suppressedExceptions: List<Throwable>
} }
private fun Throwable.dumpStackTraceTo(indent: String, qualifier: String, target: StringBuilder) { private fun Array<Throwable>.hasSeen(exception: Throwable): Boolean = any { it === exception }
this.dumpSelfTrace(indent, qualifier, target)
private fun Throwable.dumpStackTraceTo(indent: String, qualifier: String, target: StringBuilder, visited: Array<Throwable>) {
this.dumpSelfTrace(indent, qualifier, target, visited) || return
var cause = this.cause var cause = this.cause
while (cause != null) { while (cause != null) {
// TODO: should skip common stack frames // TODO: should skip common stack frames
cause.dumpSelfTrace(indent, "Caused by: ", target) cause.dumpSelfTrace(indent, "Caused by: ", target, visited)
cause = cause.cause 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<Throwable>): Boolean {
target.append(indent).append(qualifier) 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? val stack = this.asDynamic().stack as String?
if (stack != null) { if (stack != null) {
if (indent.isNotEmpty()) { if (indent.isNotEmpty()) {
@@ -76,7 +84,8 @@ private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, target: S
if (suppressed.isNotEmpty()) { if (suppressed.isNotEmpty()) {
val suppressedIndent = indent + '\t' val suppressedIndent = indent + '\t'
for (s in suppressed) { for (s in suppressed) {
s.dumpStackTraceTo(suppressedIndent, "Suppressed: ", target) s.dumpStackTraceTo(suppressedIndent, "Suppressed: ", target, visited)
} }
} }
return true
} }
+26
View File
@@ -127,6 +127,32 @@ class ExceptionTest {
assertInTrace(causeSuppressed) 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 // fail(topLevelTrace) // to dump the entire trace
} }