// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: error_formatter.go
// Source: runtime_error.go
// Source: ast/location.go
// Copyright 2017 Google Inc. All rights reserved.
// Modifications: Adapted trace cropping and source/color rendering to MoonBit diagnostics.
// License and retained attribution: LICENSE and NOTICE at the module root.
///|
/// Source spans and synthetic runtime sites share the same trace representation.
pub(all) enum TraceLocation {
Source(SourceRange)
Synthetic(String)
} derive(Eq, Debug)
///|
pub extend TraceLocation with Eq::{equal, not_equal}
///|
pub extend TraceLocation with @debug.Debug::{to_repr}
///|
pub(all) struct TraceFrame {
location : TraceLocation
name : String
} derive(Eq, Debug)
///|
pub extend TraceFrame with Eq::{equal, not_equal}
///|
pub extend TraceFrame with @debug.Debug::{to_repr}
///|
pub fn TraceLocation::format(self : TraceLocation) -> String {
match self {
Source(range) => range.format()
Synthetic(message) => message
}
}
///|
/// Go-style error rendering is separate from the legacy Diagnostic::format.
/// Source text is supplied by the host. Color transforms the selected static
/// source fragment; runtime traces do not invoke color, matching pinned Go.
pub struct DiagnosticFormatter {
priv mut max_trace : Int
priv mut color : ((String) -> String)?
priv sources : Map[String, String]
}
///|
pub fn DiagnosticFormatter::new() -> DiagnosticFormatter {
{ max_trace: 20, color: None, sources: Map([]), }
}
///|
pub fn DiagnosticFormatter::set_max_trace(
self : DiagnosticFormatter,
size : Int,
) -> Unit {
self.max_trace = size
}
///|
pub fn DiagnosticFormatter::set_color(
self : DiagnosticFormatter,
color? : (String) -> String,
) -> Unit {
self.color = color
}
///|
pub fn DiagnosticFormatter::set_source(
self : DiagnosticFormatter,
source : Source,
) -> Unit {
self.sources[source.id] = source.text
}
///|
pub fn DiagnosticFormatter::clear_sources(self : DiagnosticFormatter) -> Unit {
self.sources.clear()
}
///|
/// Configure subsequent explicit error rendering without changing execution or
/// import caches. Omit formatter to restore a fresh default renderer per call.
pub fn Evaluator::set_diagnostic_formatter(
self : Evaluator,
formatter? : DiagnosticFormatter,
) -> Unit raise JsonnetError {
self.idle()
self.diagnostic_formatter = formatter
}
///|
pub fn Evaluator::format_error(
self : Evaluator,
diagnostic : Diagnostic,
) -> String {
self.diagnostic_formatter
.unwrap_or_else(DiagnosticFormatter::new)
.format(diagnostic)
}
///|
pub fn Evaluator::format_error_bytes(
self : Evaluator,
diagnostic : Diagnostic,
) -> Bytes {
self.diagnostic_formatter
.unwrap_or_else(DiagnosticFormatter::new)
.format_bytes(diagnostic)
}
///|
pub fn DiagnosticFormatter::format(
self : DiagnosticFormatter,
diagnostic : Diagnostic,
) -> String {
@utf8.decode_lossy(self.format_bytes(diagnostic), ignore_bom=false)
}
///|
/// Preserve malformed-Unicode diagnostic bytes, which pinned Go may truncate
/// inside a UTF-8 character. The String API normalizes those bytes instead.
pub fn DiagnosticFormatter::format_bytes(
self : DiagnosticFormatter,
diagnostic : Diagnostic,
) -> Bytes {
if (diagnostic.kind == Syntax || diagnostic.kind == Static) &&
diagnostic.trace.any(frame => frame.location is Synthetic(_)) {
let source = self.sources.get(diagnostic.location.file).unwrap_or("")
let output = b"RUNTIME ERROR: ".to_array()
output.append(diagnostic.format_syntax_bytes(source).to_array())
output.append(b"\n".to_array())
let frames = @utf8.encode(
self.runtime_text({ ..diagnostic, kind: Runtime, message: "", }),
)
output.append(frames[b"RUNTIME ERROR: \n".length():].to_array())
return Bytes::from_array(output)
}
if diagnostic.kind == Syntax || diagnostic.kind == Static {
let source = self.sources.get(diagnostic.location.file)
let header = diagnostic.format_syntax_bytes(source.unwrap_or(""))
let result = header.to_array()
result.append(b"\n".to_array())
if source is Some(source) {
let range = diagnostic
.syntax_error_site(source)
.map(site => site.1)
.unwrap_or({ begin: diagnostic.location, end: diagnostic.location, })
if diagnostic_source_parts(source, range)
is Some((before, selected, after)) {
result.append(
@utf8.encode(
"\n" +
before +
self.color.map(color => color(selected)).unwrap_or(selected) +
after +
"\n",
).to_array(),
)
}
}
result.append(b"\n".to_array())
return Bytes::from_array(result)
}
@utf8.encode(self.runtime_text(diagnostic))
}
///|
fn DiagnosticFormatter::runtime_text(
self : DiagnosticFormatter,
diagnostic : Diagnostic,
) -> String {
let output = StringBuilder()
output.write_string("RUNTIME ERROR: " + diagnostic.message + "\n")
let size = diagnostic.trace.length()
let above = self.max_trace / 2
let below = self.max_trace - above
let mut index = 0
while index < size {
if self.max_trace > 0 && index >= above && index < size - below {
output.write_string("\t...\n")
index = size - below
} else {
let frame = diagnostic.trace[size - index - 1]
output.write_string(
"\t" + frame.location.format() + "\t" + frame.name + "\n",
)
index += 1
}
}
output.to_string()
}
///|
fn diagnostic_source_parts(
source : String,
range : SourceRange,
) -> (String, String, String)? {
let bytes = @utf8.encode(source)
if bytes.length() > SyntaxLimits::default().source_bytes ||
range.begin.line < 1 ||
range.begin.column < 1 ||
range.end.line < range.begin.line ||
range.end.column < 1 {
return None
}
let mut line = 1
let mut line_start = 0
let mut first_line = -1
let mut selected_start = -1
let mut selected_end = -1
let mut last_line = -1
for index = 0; index <= bytes.length(); index = index + 1 {
if index == bytes.length() || bytes[index] == 10 {
if line == range.begin.line &&
range.begin.column - 1 <= index - line_start {
first_line = line_start
selected_start = line_start + range.begin.column - 1
}
if line == range.end.line && range.end.column - 1 <= index - line_start {
selected_end = line_start + range.end.column - 1
last_line = index
break
}
line += 1
line_start = index + 1
}
}
if first_line < 0 ||
selected_start < 0 ||
selected_end < selected_start ||
last_line < selected_end {
return None
}
Some(
(
@utf8.decode(bytes[first_line:selected_start], ignore_bom=false),
@utf8.decode(bytes[selected_start:selected_end], ignore_bom=false),
@utf8.decode(bytes[selected_end:last_line], ignore_bom=false),
),
) catch {
_ => None
}
}