Skip to content

Trim stack traces in REPL #10717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions compiler/src/dotty/tools/dotc/util/StackTraceOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package dotty.tools.dotc.util

import collection.mutable, mutable.ListBuffer
import scala.util.chaining.given
import java.lang.System.lineSeparator

object StackTraceOps:

extension (t: Throwable)

/** Format a stack trace, taking the prefix span satisfying a predicate.
*
* The format is similar to the typical case described in the Javadoc
* for [[java.lang.Throwable#printStackTrace()*]].
* If a stack trace is truncated, it will be followed by a line of the form
* `... 3 elided`, by analogy to the lines `... 3 more` which indicate
* shared stack trace segments.
* @param e the exception
* @param p the predicate to select the prefix
*/
def formatStackTracePrefix(p: StackTraceElement => Boolean): String =

type TraceRelation = String
val Self = new TraceRelation("")
val CausedBy = new TraceRelation("Caused by: ")
val Suppressed = new TraceRelation("Suppressed: ")

def header(e: Throwable): String =
def because = e.getCause match { case null => null ; case c => header(c) }
def msg = e.getMessage match { case null => because ; case s => s }
def txt = msg match { case null => "" ; case s => s": $s" }
s"${e.getClass.getName}$txt"

val seen = mutable.Set.empty[Throwable]
def unseen(e: Throwable): Boolean = (e != null && !seen(e)).tap(if _ then seen += e)

val lines = ListBuffer.empty[String]

// format the stack trace, skipping the shared trace
def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if unseen(e) then
val trace = e.getStackTrace
val frames = if share.isEmpty then trace else
val spare = share.reverseIterator
val trimmed = trace.reverse.dropWhile(spare.hasNext && spare.next() == _)
trimmed.reverse
val prefix = frames.takeWhile(p)
val margin = " " * indents
lines += s"${margin}${r}${header(e)}"
prefix.foreach(frame => lines += s"$margin at $frame")

val traceFramesLenDiff = trace.length - frames.length
val framesPrefixLenDiff = frames.length - prefix.length
if traceFramesLenDiff > 0 then
if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided and $traceFramesLenDiff more"
else lines += s"$margin ... $traceFramesLenDiff more"
else if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided"

print(e.getCause, CausedBy, trace, indents)
e.getSuppressed.foreach(print(_, Suppressed, frames, indents + 1))
end print

print(t, Self, share = Array.empty, indents = 0)
lines.mkString(lineSeparator)
end formatStackTracePrefix
35 changes: 18 additions & 17 deletions compiler/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import dotc.core.Denotations.Denotation
import dotc.core.Flags
import dotc.core.Flags._
import dotc.core.Symbols.{Symbol, defn}
import dotc.core.StdNames.str
import dotc.core.StdNames.{nme, str}
import dotc.core.NameOps._
import dotc.printing.ReplPrinter
import dotc.reporting.{MessageRendering, Message, Diagnostic}
Expand Down Expand Up @@ -115,32 +115,33 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
/** Render value definition result */
def renderVal(d: Denotation)(using Context): Option[Diagnostic] =
val dcl = d.symbol.showUser

def msg(s: String) = infoDiagnostic(s, d)
try
if (d.symbol.is(Flags.Lazy)) Some(infoDiagnostic(dcl, d))
else valueOf(d.symbol).map(value => infoDiagnostic(s"$dcl = $value", d))
catch case ex: InvocationTargetException => Some(infoDiagnostic(renderError(ex), d))
if (d.symbol.is(Flags.Lazy)) Some(msg(dcl))
else valueOf(d.symbol).map(value => msg(s"$dcl = $value"))
catch case e: InvocationTargetException => Some(msg(renderError(e, d)))
end renderVal

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

/** Render the stack trace of the underlying exception. */
private def renderError(ex: InvocationTargetException | ExceptionInInitializerError): String = {
val cause = ex.getCause match {
case ex: ExceptionInInitializerError => ex.getCause
case ex => ex
}
val sw = new StringWriter()
val pw = new PrintWriter(sw)
cause.printStackTrace(pw)
sw.toString
}
private def renderError(ite: InvocationTargetException | ExceptionInInitializerError, d: Denotation)(using Context): String =
import dotty.tools.dotc.util.StackTraceOps._
val cause = ite.getCause match
case e: ExceptionInInitializerError => e.getCause
case e => e
def isWrapperCode(ste: StackTraceElement) =
ste.getClassName == d.symbol.owner.name.show
&& (ste.getMethodName == nme.STATIC_CONSTRUCTOR.show || ste.getMethodName == nme.CONSTRUCTOR.show)

cause.formatStackTracePrefix(!isWrapperCode(_))
end renderError

private def infoDiagnostic(msg: String, d: Denotation)(using Context): Diagnostic =
new Diagnostic.Info(msg, d.symbol.sourcePos)
Expand Down
76 changes: 76 additions & 0 deletions compiler/test/dotty/tools/dotc/util/StackTraceTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

package dotty.tools.dotc.util

import scala.util.{Failure, Success, Try}
import scala.util.chaining.given

import org.junit.Assert.{assertEquals, assertTrue}
import org.junit.Test

class StackTraceTest:
val CausedBy = "Caused by: "
val Suppressed = "Suppressed: "

// throws
def sample = throw new RuntimeException("Point of failure")
def sampler: String = sample

// repackage with message
def resample: String = try sample catch case e: Throwable => throw new RuntimeException("resample", e)
def resampler: String = resample

// simple wrapper
def wrapper: String = try sample catch case e: Throwable => throw new RuntimeException(e)
// another onion skin
def rewrapper: String = try wrapper catch case e: Throwable => throw new RuntimeException(e)
def rewrapperer: String = rewrapper

// circular cause
def insane: String = try sample catch case e: Throwable => throw new RuntimeException(e).tap(e.initCause)
def insaner: String = insane

def repressed: String = try sample catch case e: Throwable => throw new RuntimeException("My problem").tap(_.addSuppressed(e))
def represser: String = repressed

// evaluating s should throw, p trims stack trace, t is the test of resulting trace string
def probe(s: => String)(p: StackTraceElement => Boolean)(t: String => Unit): Unit =
import StackTraceOps.formatStackTracePrefix
Try(s).recover { case e => e.formatStackTracePrefix(p) } match
case Success(s) => t(s)
case Failure(e) => throw e

@Test def showsAllTrace() =
probe(sampler)(_ => true)(s => assertTrue(s.linesIterator.length > 5))

// summary + one frame + elision
@Test def showsOnlyPrefix() =
probe(sample)(_.getMethodName == "sample")(s => assertEquals(3, s.linesIterator.length))

// summary + one frame + elision, caused by + one frame + elision
@Test def showsCause() = probe(resampler)(_.getMethodName != "resampler") { s =>
val res = s.linesIterator.toList
assertEquals(6, res.length)
assertTrue(res.exists(_.startsWith(CausedBy)))
}

// summary + one frame + elision times three
@Test def showsWrappedExceptions() = probe(rewrapperer)(_.getMethodName != "rewrapperer") { s =>
val res = s.linesIterator.toList
assertEquals(9, res.length)
assertTrue(res.exists(_.startsWith(CausedBy)))
assertEquals(2, res.collect { case s if s.startsWith(CausedBy) => s }.size)
}

// summary + one frame + elision times two with extra frame
@Test def dontBlowOnCycle() = probe(insaner)(_.getMethodName != "insaner") { s =>
val res = s.linesIterator.toList
assertEquals(6, res.length)
assertTrue(res.exists(_.startsWith(CausedBy)))
}

@Test def showsSuppressed() = probe(represser)(_.getMethodName != "represser") { s =>
val res = s.linesIterator.toList
assertEquals(6, res.length)
assertTrue(res.exists(_.trim.startsWith(Suppressed)))
}
end StackTraceTest