Skip to content
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

Error handling improvements #125

Merged
merged 5 commits into from
Aug 9, 2021
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
8 changes: 3 additions & 5 deletions sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import java.io.{BufferedOutputStream, InputStream, OutputStreamWriter, PrintStre
import java.nio.charset.StandardCharsets
import java.nio.file.NoSuchFileException


import scala.collection.mutable
import scala.util.Try
import scala.util.control.NonFatal

object SjsonnetMain {
def createParseCache() = collection.mutable.HashMap[(Path, String), Either[String, (Expr, FileScope)]]()
def createParseCache() = collection.mutable.HashMap[(Path, String), Either[Error, (Expr, FileScope)]]()

def resolveImport(searchRoots0: Seq[Path], allowedInputs: Option[Set[os.Path]] = None) = new Importer {
def resolve(docBase: Path, importName: String): Option[Path] =
Expand Down Expand Up @@ -46,7 +44,7 @@ object SjsonnetMain {
}

def main0(args: Array[String],
parseCache: collection.mutable.HashMap[(Path, String), Either[String, (Expr, FileScope)]],
parseCache: collection.mutable.HashMap[(Path, String), Either[Error, (Expr, FileScope)]],
stdin: InputStream,
stdout: PrintStream,
stderr: PrintStream,
Expand Down Expand Up @@ -129,7 +127,7 @@ object SjsonnetMain {

def mainConfigured(file: String,
config: Config,
parseCache: collection.mutable.HashMap[(Path, String), Either[String, (Expr, FileScope)]],
parseCache: collection.mutable.HashMap[(Path, String), Either[Error, (Expr, FileScope)]],
wd: os.Path,
allowedInputs: Option[Set[os.Path]] = None,
importer: Option[(Path, String) => Option[os.Path]] = None): Either[String, String] = {
Expand Down
119 changes: 74 additions & 45 deletions sjsonnet/src/sjsonnet/Error.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,90 @@ import scala.util.control.NonFatal
* propagating upwards. This helps provide good error messages with line
* numbers pointing towards user code.
*/
case class Error(msg: String,
stack: List[StackTraceElement],
underlying: Option[Throwable])
extends Exception(msg, underlying.orNull){
setStackTrace(stack.toArray.reverse)
def addFrame(pos: Position, wd: Path)(implicit ev: EvalErrorScope) = {
val newFrame = ev.prettyIndex(pos) match {
case None =>
new StackTraceElement(
"", "",
pos.currentFile.relativeToString(wd) + " offset:",
pos.offset
)
case Some((line, col)) =>

new StackTraceElement(
"", "",
pos.currentFile.relativeToString(wd) + ":" + line,
col.toInt
)
}
class Error(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
extends Exception(msg, underlying.orNull) {

setStackTrace(stack.reverseIterator.map(_.ste).toArray)

override def fillInStackTrace: Throwable = this

this.copy(stack = newFrame :: this.stack)
def addFrame(pos: Position, expr: Expr = null)(implicit ev: EvalErrorScope): Error = {
if(stack.isEmpty || alwaysAddPos(expr)) {
val exprErrorString = if(expr == null) null else expr.exprErrorString
val newFrame = new Error.Frame(pos, exprErrorString)
stack match {
case s :: ss if s.pos == pos =>
if(s.exprErrorString == null && exprErrorString != null) copy(stack = newFrame :: ss)
else this
case _ => copy(stack = newFrame :: stack)
}
} else this
}
}

def asSeenFrom(ev: EvalErrorScope): Error =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

copy(stack = stack.map(_.asSeenFrom(ev)))

protected[this] def copy(msg: String = msg, stack: List[Error.Frame] = stack,
underlying: Option[Throwable] = underlying) =
new Error(msg, stack, underlying)

private[this] def alwaysAddPos(expr: Expr): Boolean = expr match {
case _: Expr.LocalExpr | _: Expr.Arr | _: Expr.ObjExtend | _: Expr.ObjBody | _: Expr.IfElse => false
case _ => true
}
}

object Error {
def tryCatch[T](pos: Position)
(implicit evaluator: EvalErrorScope): PartialFunction[Throwable, Nothing] = {
case e: Error => throw e
case e: Error.Delegate =>
throw new Error(e.msg, Nil, None).addFrame(pos, evaluator.wd)
case NonFatal(e) =>
throw new Error("Internal Error", Nil, Some(e)).addFrame(pos, evaluator.wd)
final class Frame(val pos: Position, val exprErrorString: String)(implicit ev: EvalErrorScope) {
val ste: StackTraceElement = {
val cl = if(exprErrorString == null) "" else s"[${exprErrorString}]"
val (frameFile, frameLine) = ev.prettyIndex(pos) match {
case None => (pos.currentFile.relativeToString(ev.wd) + " offset:", pos.offset)
case Some((line, col)) => (pos.currentFile.relativeToString(ev.wd) + ":" + line, col.toInt)
}
new StackTraceElement(cl, "", frameFile, frameLine)
}

def asSeenFrom(ev: EvalErrorScope): Frame =
if(ev eq this.ev) this else new Frame(pos, exprErrorString)(ev)
}
def tryCatchWrap[T](pos: Position)
(implicit evaluator: EvalErrorScope): PartialFunction[Throwable, Nothing] = {
case e: Error => throw e.addFrame(pos, evaluator.wd)
case e: Error.Delegate =>
throw new Error(e.msg, Nil, None).addFrame(pos, evaluator.wd)

def withStackFrame[T](expr: Expr)
(implicit evaluator: EvalErrorScope): PartialFunction[Throwable, Nothing] = {
case e: Error => throw e.addFrame(expr.pos, expr)
case NonFatal(e) =>
throw new Error("Internal Error", Nil, Some(e)).addFrame(pos, evaluator.wd)
}
def fail(msg: String, pos: Position)
(implicit evaluator: EvalErrorScope) = {
throw Error(msg, Nil, None).addFrame(pos, evaluator.wd)
throw new Error("Internal Error", Nil, Some(e)).addFrame(expr.pos, expr)
}

/**
* An exception containing a message, which is expected to get caught by
* the nearest enclosing try-catch and converted into an [[Error]]
*/
case class Delegate(msg: String) extends Exception(msg)
def fail(msg: String, expr: Expr)(implicit ev: EvalErrorScope): Nothing =
fail(msg, expr.pos, expr.exprErrorString)

def fail(msg: String, pos: Position, cl: String = null)(implicit ev: EvalErrorScope): Nothing =
throw new Error(msg, new Frame(pos, cl) :: Nil, None)

def fail(msg: String): Nothing =
throw new Error(msg)
}

class ParseError(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
extends Error(msg, stack, underlying) {

override protected[this] def copy(msg: String = msg, stack: List[Error.Frame] = stack,
underlying: Option[Throwable] = underlying) =
new ParseError(msg, stack, underlying)
}

class StaticError(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
extends Error(msg, stack, underlying) {

override protected[this] def copy(msg: String = msg, stack: List[Error.Frame] = stack,
underlying: Option[Throwable] = underlying) =
new StaticError(msg, stack, underlying)
}

object StaticError {
def fail(msg: String, expr: Expr)(implicit ev: EvalErrorScope): Nothing =
throw new StaticError(msg, new Error.Frame(expr.pos, expr.exprErrorString) :: Nil, None)
}

trait EvalErrorScope {
Expand Down
Loading