///|
/// Parser coordinates, interpreted according to the warning's producer.
#derive(Eq, Debug)
pub(all) struct Span {
start_line : Int32
start_column : Int32
end_line : Int32
end_column : Int32
}
///|
/// Authored spelling and final allocation stay separate in each related location.
#derive(Eq, Debug)
pub(all) struct Location {
owner : String
original : String
allocated : String
source : Span?
}
///|
/// A recoverable diagnostic independent of optional observation sinks.
#derive(Eq, Debug)
pub(all) struct Warning {
code : String
stage : String
message : String
locations : Array[Location]
fix : String
}
///|
#derive(Debug)
pub(all) suberror DiagnosticError {
InvalidDiagnostic(String)
}
///|
pub extend Span with Eq::{equal, not_equal}
///|
pub extend Span with @debug.Debug::{to_repr}
///|
pub extend Location with Eq::{equal, not_equal}
///|
pub extend Location with @debug.Debug::{to_repr}
///|
pub extend Warning with Eq::{equal, not_equal}
///|
pub extend Warning with @debug.Debug::{to_repr}
///|
pub extend DiagnosticError with @debug.Debug::{to_repr}
///|
fn record(fields : Array[(String, @ion.IonValue)]) -> @ion.IonValue {
@ion.Ion(@ion.IonValueKind::Struct(fields.map(p => @ion.field(p.0, p.1))))
}
///|
pub fn Warning::to_ion(self : Warning) -> @ion.IonValue {
record([
("contract", @ion.IonValue::string("morphir-warning-v1")),
("severity", @ion.IonValue::string("warning")),
("code", @ion.IonValue::string(self.code)),
("stage", @ion.IonValue::string(self.stage)),
("message", @ion.IonValue::string(self.message)),
("fix", @ion.IonValue::string(self.fix)),
(
"locations",
@ion.IonValue::list(
self.locations.map(loc => {
record([
("owner", @ion.IonValue::string(loc.owner)),
("original", @ion.IonValue::string(loc.original)),
("allocated", @ion.IonValue::string(loc.allocated)),
(
"source",
match loc.source {
None => @ion.Ion(@ion.IonValueKind::Null(@ion.Null))
Some(s) =>
record([
("start_line", @ion.IonValue::int(s.start_line)),
("start_column", @ion.IonValue::int(s.start_column)),
("end_line", @ion.IonValue::int(s.end_line)),
("end_column", @ion.IonValue::int(s.end_column)),
])
},
),
])
}),
),
),
]).with_annotation(@ion.symbol("morphir_warning"))
}
///|
fn fields(
value : @ion.IonValue,
expected : Array[String],
) -> Map[String, @ion.IonValue] raise DiagnosticError {
guard value.kind() is Struct(items) else {
raise InvalidDiagnostic("Expected a diagnostic record")
}
let result : Map[String, @ion.IonValue] = Map([])
for item in items {
guard item.name().text() is Some(name) else {
raise InvalidDiagnostic("Unresolved diagnostic field")
}
if !expected.contains(name) ||
result.contains(name) ||
item.value().has_annotations() {
raise InvalidDiagnostic("Invalid diagnostic field: " + name)
}
result[name] = item.value()
}
if result.length() != expected.length() {
raise InvalidDiagnostic("Missing diagnostic field")
}
result
}
///|
fn text(
fields : Map[String, @ion.IonValue],
name : String,
) -> String raise DiagnosticError {
guard fields[name].kind() is String(value) else {
raise InvalidDiagnostic("Expected diagnostic string: " + name)
}
value
}
///|
fn number(
fields : Map[String, @ion.IonValue],
name : String,
) -> Int32 raise DiagnosticError {
guard fields[name].kind() is Int(value) else {
raise InvalidDiagnostic("Expected diagnostic coordinate: " + name)
}
let n = @strconv.parse_int(value.to_string()) catch {
_ => raise InvalidDiagnostic("Invalid diagnostic coordinate: " + name)
}
guard n >= 0 else {
raise InvalidDiagnostic("Invalid diagnostic coordinate: " + name)
}
n
}
///|
/// Unknown provenance is preserved by the caller and is not a warning.
pub fn warning_from_ion(
value : @ion.IonValue,
) -> Warning? raise DiagnosticError {
if !value.annotations().any(a => a.text() == Some("morphir_warning")) {
return None
}
if value.annotations().length() != 1 {
raise InvalidDiagnostic("Unexpected warning annotation")
}
let f = fields(value, [
"contract", "severity", "code", "stage", "message", "locations", "fix",
])
if text(f, "contract") != "morphir-warning-v1" ||
text(f, "severity") != "warning" {
raise InvalidDiagnostic("Unsupported warning contract")
}
guard f["locations"].kind() is List(items) else {
raise InvalidDiagnostic("Expected warning locations")
}
let locations = items.map(item => {
if item.has_annotations() {
raise InvalidDiagnostic("Annotated warning location")
}
let loc = fields(item, ["owner", "original", "allocated", "source"])
let source = if loc["source"].kind() is Null(@ion.Null) {
None
} else {
let s = fields(loc["source"], [
"start_line", "start_column", "end_line", "end_column",
])
let span : Span = {
start_line: number(s, "start_line"),
start_column: number(s, "start_column"),
end_line: number(s, "end_line"),
end_column: number(s, "end_column"),
}
if span.end_line < span.start_line ||
(
span.end_line == span.start_line &&
span.end_column < span.start_column
) {
raise InvalidDiagnostic("Reversed diagnostic span")
}
Some(span)
}
Location::{
owner: text(loc, "owner"),
original: text(loc, "original"),
allocated: text(loc, "allocated"),
source,
}
})
Some({
code: text(f, "code"),
stage: text(f, "stage"),
message: text(f, "message"),
locations,
fix: text(f, "fix"),
})
}
///|
/// Length prefixes prevent separator-bearing owner strings from aliasing keys.
pub fn Warning::key(self : Warning) -> String {
let parts = [self.code, self.stage]
for loc in self.locations {
parts.push(loc.owner)
parts.push(loc.allocated)
}
parts.map(p => p.length().to_string() + ":" + p).join("")
}
///|
pub fn Warning::display(self : Warning) -> String {
let lines = [self.code + ": " + self.message]
for loc in self.locations {
let span = match loc.source {
Some(s) =>
":" + s.start_line.to_string() + ":" + s.start_column.to_string()
None => ""
}
lines.push(
" " + loc.owner + span + ": " + loc.original + " -> " + loc.allocated,
)
}
lines.push(" fix: " + self.fix)
lines.join("\n")
}