///|
/// Escapes a string for embedding in JSON while preserving valid UTF-8.
fn hex_digit(value : Byte) -> Char {
match value {
0 => '0'
1 => '1'
2 => '2'
3 => '3'
4 => '4'
5 => '5'
6 => '6'
7 => '7'
8 => '8'
9 => '9'
10 => 'a'
11 => 'b'
12 => 'c'
13 => 'd'
14 => 'e'
_ => 'f'
}
}
///|
/// Escapes a string for embedding in JSON while preserving valid UTF-8.
fn write_json_string(output : StringBuilder, text : String) -> Unit {
let bytes = @utf8.encode(text)
output.write_char('"')
let mut offset = 0
while offset < bytes.length() {
let byte = bytes[offset]
match byte {
0x22 => {
output.write_string("\\\"")
offset += 1
}
0x5c => {
output.write_string("\\\\")
offset += 1
}
0x08 => {
output.write_string("\\b")
offset += 1
}
0x0c => {
output.write_string("\\f")
offset += 1
}
0x0a => {
output.write_string("\\n")
offset += 1
}
0x0d => {
output.write_string("\\r")
offset += 1
}
0x09 => {
output.write_string("\\t")
offset += 1
}
_ =>
if byte < 0x20 {
output.write_string("\\u00")
output.write_char(hex_digit(byte / 16))
output.write_char(hex_digit(byte % 16))
offset += 1
} else {
let count = utf8_sequence_length(byte)
let end = if offset + count <= bytes.length() {
offset + count
} else {
offset + 1
}
output.write_string(@utf8.decode_lossy(bytes[offset:end]))
offset = end
}
}
}
output.write_char('"')
}
///|
fn write_optional_json_string(output : StringBuilder, value : String?) -> Unit {
match value {
Some(text) => write_json_string(output, text)
None => output.write_string("null")
}
}
///|
fn write_json_string_array(
output : StringBuilder,
values : Array[String],
) -> Unit {
output.write_char('[')
for index, value in values {
if index > 0 {
output.write_char(',')
}
write_json_string(output, value)
}
output.write_char(']')
}
///|
fn write_json_label(
output : StringBuilder,
sources : SourceMap,
label : Label,
) -> Unit {
output.write_string("{\"source_id\":")
output.write_string(label.source().value().to_string())
output.write_string(",\"source_name\":")
match sources.get(label.source()) {
Some(source) => write_json_string(output, source.name())
None => output.write_string("null")
}
output.write_string(",\"start\":")
output.write_string(label.span().start().to_string())
output.write_string(",\"end\":")
output.write_string(label.span().end().to_string())
output.write_string(",\"primary\":")
output.write_string(if label.is_primary() { "true" } else { "false" })
output.write_string(",\"message\":")
write_json_string(output, label.message())
match sources.get(label.source()) {
Some(source) => {
let start = source.location(label.span().start())
let end = source.location(label.span().end())
output.write_string(",\"start_line\":")
output.write_string((start.line() + 1).to_string())
output.write_string(",\"start_column\":")
output.write_string((start.column() + 1).to_string())
output.write_string(",\"end_line\":")
output.write_string((end.line() + 1).to_string())
output.write_string(",\"end_column\":")
output.write_string((end.column() + 1).to_string())
}
None => ()
}
output.write_char('}')
}
///|
/// Serializes one diagnostic to a stable, compact JSON object.
///
/// Byte offsets are zero-based. Human-facing line and column values are
/// one-based. Unknown source IDs remain representable with a null name.
pub fn render_json(sources : SourceMap, diagnostic : Diagnostic) -> String {
let output = StringBuilder::new()
output.write_string("{\"severity\":")
write_json_string(output, diagnostic.severity().name())
output.write_string(",\"message\":")
write_json_string(output, diagnostic.message())
output.write_string(",\"code\":")
write_optional_json_string(output, diagnostic.code())
output.write_string(",\"help\":")
write_optional_json_string(output, diagnostic.help())
output.write_string(",\"notes\":")
write_json_string_array(output, diagnostic.notes())
output.write_string(",\"labels\":[")
for index, label in diagnostic.labels() {
if index > 0 {
output.write_char(',')
}
write_json_label(output, sources, label)
}
output.write_string("]}")
output.to_string()
}
///|
/// Emits newline-delimited JSON for streaming into editors and CI systems.
pub fn render_json_lines(
sources : SourceMap,
diagnostics : Array[Diagnostic],
) -> String {
let output = StringBuilder::new()
for index, diagnostic in diagnostics {
if index > 0 {
output.write_char('\n')
}
output.write_string(render_json(sources, diagnostic))
}
output.to_string()
}