Skip to content

Commit 600c25c

Browse files
authored
Merge pull request #10717 from som-snytt/forward/stack-formatter
2 parents afc6396 + f1b53d3 commit 600c25c

File tree

3 files changed

+170
-17
lines changed

3 files changed

+170
-17
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package dotty.tools.dotc.util
14+
15+
import collection.mutable, mutable.ListBuffer
16+
import scala.util.chaining.given
17+
import java.lang.System.lineSeparator
18+
19+
object StackTraceOps:
20+
21+
extension (t: Throwable)
22+
23+
/** Format a stack trace, taking the prefix span satisfying a predicate.
24+
*
25+
* The format is similar to the typical case described in the Javadoc
26+
* for [[java.lang.Throwable#printStackTrace()*]].
27+
* If a stack trace is truncated, it will be followed by a line of the form
28+
* `... 3 elided`, by analogy to the lines `... 3 more` which indicate
29+
* shared stack trace segments.
30+
* @param e the exception
31+
* @param p the predicate to select the prefix
32+
*/
33+
def formatStackTracePrefix(p: StackTraceElement => Boolean): String =
34+
35+
type TraceRelation = String
36+
val Self = new TraceRelation("")
37+
val CausedBy = new TraceRelation("Caused by: ")
38+
val Suppressed = new TraceRelation("Suppressed: ")
39+
40+
def header(e: Throwable): String =
41+
def because = e.getCause match { case null => null ; case c => header(c) }
42+
def msg = e.getMessage match { case null => because ; case s => s }
43+
def txt = msg match { case null => "" ; case s => s": $s" }
44+
s"${e.getClass.getName}$txt"
45+
46+
val seen = mutable.Set.empty[Throwable]
47+
def unseen(e: Throwable): Boolean = (e != null && !seen(e)).tap(if _ then seen += e)
48+
49+
val lines = ListBuffer.empty[String]
50+
51+
// format the stack trace, skipping the shared trace
52+
def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if unseen(e) then
53+
val trace = e.getStackTrace
54+
val frames = if share.isEmpty then trace else
55+
val spare = share.reverseIterator
56+
val trimmed = trace.reverse.dropWhile(spare.hasNext && spare.next() == _)
57+
trimmed.reverse
58+
val prefix = frames.takeWhile(p)
59+
val margin = " " * indents
60+
lines += s"${margin}${r}${header(e)}"
61+
prefix.foreach(frame => lines += s"$margin at $frame")
62+
63+
val traceFramesLenDiff = trace.length - frames.length
64+
val framesPrefixLenDiff = frames.length - prefix.length
65+
if traceFramesLenDiff > 0 then
66+
if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided and $traceFramesLenDiff more"
67+
else lines += s"$margin ... $traceFramesLenDiff more"
68+
else if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided"
69+
70+
print(e.getCause, CausedBy, trace, indents)
71+
e.getSuppressed.foreach(print(_, Suppressed, frames, indents + 1))
72+
end print
73+
74+
print(t, Self, share = Array.empty, indents = 0)
75+
lines.mkString(lineSeparator)
76+
end formatStackTracePrefix

compiler/src/dotty/tools/repl/Rendering.scala

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import dotc.core.Denotations.Denotation
1111
import dotc.core.Flags
1212
import dotc.core.Flags._
1313
import dotc.core.Symbols.{Symbol, defn}
14-
import dotc.core.StdNames.str
14+
import dotc.core.StdNames.{nme, str}
1515
import dotc.core.NameOps._
1616
import dotc.printing.ReplPrinter
1717
import dotc.reporting.{MessageRendering, Message, Diagnostic}
@@ -115,32 +115,33 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
115115
/** Render value definition result */
116116
def renderVal(d: Denotation)(using Context): Option[Diagnostic] =
117117
val dcl = d.symbol.showUser
118-
118+
def msg(s: String) = infoDiagnostic(s, d)
119119
try
120-
if (d.symbol.is(Flags.Lazy)) Some(infoDiagnostic(dcl, d))
121-
else valueOf(d.symbol).map(value => infoDiagnostic(s"$dcl = $value", d))
122-
catch case ex: InvocationTargetException => Some(infoDiagnostic(renderError(ex), d))
120+
if (d.symbol.is(Flags.Lazy)) Some(msg(dcl))
121+
else valueOf(d.symbol).map(value => msg(s"$dcl = $value"))
122+
catch case e: InvocationTargetException => Some(msg(renderError(e, d)))
123123
end renderVal
124124

125125
/** Force module initialization in the absence of members. */
126126
def forceModule(sym: Symbol)(using Context): Seq[Diagnostic] =
127127
def load() =
128128
val objectName = sym.fullName.encode.toString
129-
val resObj: Class[?] = Class.forName(objectName, true, classLoader())
129+
Class.forName(objectName, true, classLoader())
130130
Nil
131-
try load() catch case e: ExceptionInInitializerError => List(infoDiagnostic(renderError(e), sym.denot))
131+
try load() catch case e: ExceptionInInitializerError => List(infoDiagnostic(renderError(e, sym.denot), sym.denot))
132132

133133
/** Render the stack trace of the underlying exception. */
134-
private def renderError(ex: InvocationTargetException | ExceptionInInitializerError): String = {
135-
val cause = ex.getCause match {
136-
case ex: ExceptionInInitializerError => ex.getCause
137-
case ex => ex
138-
}
139-
val sw = new StringWriter()
140-
val pw = new PrintWriter(sw)
141-
cause.printStackTrace(pw)
142-
sw.toString
143-
}
134+
private def renderError(ite: InvocationTargetException | ExceptionInInitializerError, d: Denotation)(using Context): String =
135+
import dotty.tools.dotc.util.StackTraceOps._
136+
val cause = ite.getCause match
137+
case e: ExceptionInInitializerError => e.getCause
138+
case e => e
139+
def isWrapperCode(ste: StackTraceElement) =
140+
ste.getClassName == d.symbol.owner.name.show
141+
&& (ste.getMethodName == nme.STATIC_CONSTRUCTOR.show || ste.getMethodName == nme.CONSTRUCTOR.show)
142+
143+
cause.formatStackTracePrefix(!isWrapperCode(_))
144+
end renderError
144145

145146
private def infoDiagnostic(msg: String, d: Denotation)(using Context): Diagnostic =
146147
new Diagnostic.Info(msg, d.symbol.sourcePos)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
package dotty.tools.dotc.util
3+
4+
import scala.util.{Failure, Success, Try}
5+
import scala.util.chaining.given
6+
7+
import org.junit.Assert.{assertEquals, assertTrue}
8+
import org.junit.Test
9+
10+
class StackTraceTest:
11+
val CausedBy = "Caused by: "
12+
val Suppressed = "Suppressed: "
13+
14+
// throws
15+
def sample = throw new RuntimeException("Point of failure")
16+
def sampler: String = sample
17+
18+
// repackage with message
19+
def resample: String = try sample catch case e: Throwable => throw new RuntimeException("resample", e)
20+
def resampler: String = resample
21+
22+
// simple wrapper
23+
def wrapper: String = try sample catch case e: Throwable => throw new RuntimeException(e)
24+
// another onion skin
25+
def rewrapper: String = try wrapper catch case e: Throwable => throw new RuntimeException(e)
26+
def rewrapperer: String = rewrapper
27+
28+
// circular cause
29+
def insane: String = try sample catch case e: Throwable => throw new RuntimeException(e).tap(e.initCause)
30+
def insaner: String = insane
31+
32+
def repressed: String = try sample catch case e: Throwable => throw new RuntimeException("My problem").tap(_.addSuppressed(e))
33+
def represser: String = repressed
34+
35+
// evaluating s should throw, p trims stack trace, t is the test of resulting trace string
36+
def probe(s: => String)(p: StackTraceElement => Boolean)(t: String => Unit): Unit =
37+
import StackTraceOps.formatStackTracePrefix
38+
Try(s).recover { case e => e.formatStackTracePrefix(p) } match
39+
case Success(s) => t(s)
40+
case Failure(e) => throw e
41+
42+
@Test def showsAllTrace() =
43+
probe(sampler)(_ => true)(s => assertTrue(s.linesIterator.length > 5))
44+
45+
// summary + one frame + elision
46+
@Test def showsOnlyPrefix() =
47+
probe(sample)(_.getMethodName == "sample")(s => assertEquals(3, s.linesIterator.length))
48+
49+
// summary + one frame + elision, caused by + one frame + elision
50+
@Test def showsCause() = probe(resampler)(_.getMethodName != "resampler") { s =>
51+
val res = s.linesIterator.toList
52+
assertEquals(6, res.length)
53+
assertTrue(res.exists(_.startsWith(CausedBy)))
54+
}
55+
56+
// summary + one frame + elision times three
57+
@Test def showsWrappedExceptions() = probe(rewrapperer)(_.getMethodName != "rewrapperer") { s =>
58+
val res = s.linesIterator.toList
59+
assertEquals(9, res.length)
60+
assertTrue(res.exists(_.startsWith(CausedBy)))
61+
assertEquals(2, res.collect { case s if s.startsWith(CausedBy) => s }.size)
62+
}
63+
64+
// summary + one frame + elision times two with extra frame
65+
@Test def dontBlowOnCycle() = probe(insaner)(_.getMethodName != "insaner") { s =>
66+
val res = s.linesIterator.toList
67+
assertEquals(6, res.length)
68+
assertTrue(res.exists(_.startsWith(CausedBy)))
69+
}
70+
71+
@Test def showsSuppressed() = probe(represser)(_.getMethodName != "represser") { s =>
72+
val res = s.linesIterator.toList
73+
assertEquals(6, res.length)
74+
assertTrue(res.exists(_.trim.startsWith(Suppressed)))
75+
}
76+
end StackTraceTest

0 commit comments

Comments
 (0)