use voltaic_diag::Diagnostic;
use voltaic_diag::DiagnosticCode;
use voltaic_diag::SourceMap;
use voltaic_diag::Span;
pub fn remap(
message: &RustcMessage,
map: &SourceMap,
vlt_path: &str,
vlt_source: &str,
vrs_path: &str,
vrs_source: &str,
) -> Diagnostic {
let code = DiagnosticCode::Foreign(
message
.code
.clone()
.unwrap_or_else(|| message.level.clone()),
);
let severity = if message.level == "warning" {
voltaic_diag::Severity::Warning
} else {
voltaic_diag::Severity::Error
};
match map.origin(message.line) {
Some(origin) => {
let shifted = origin.map_col(message.col);
Diagnostic {
code,
severity,
message: message.message.clone(),
span: narrow(origin.span, vlt_source, origin.src_line, shifted),
related: Vec::new(),
help: None,
path: vlt_path.to_string(),
source: vlt_source.to_string(),
}
}
None => {
let offset = generated_offset(vrs_source, message.line, message.col);
let note = format!(
"this is on a line Voltaic generated, so it has no place in your source; see {}:{}:{}",
vrs_path, message.line, message.col
);
Diagnostic {
code,
severity,
message: message.message.clone(),
span: (offset, offset + 1),
related: Vec::new(),
help: Some(note),
path: vrs_path.to_string(),
source: vrs_source.to_string(),
}
}
}
}
#[cfg(test)]
const VLT: &str = "add = (a: i32, b: i32): i32 ->\n a + \"x\"\n";
#[cfg(test)]
const VRS: &str = "fn add(a: i32, b: i32) -> i32 {\n a + \"x\"\n}\n";
#[cfg(test)]
fn map_of() -> SourceMap {
SourceMap::new(
"p.vlt",
vec![
Some(voltaic_diag::Origin {
span: (0, 30),
src_line: 1,
src_col: 1,
gen_col: 1,
}),
Some(voltaic_diag::Origin {
span: (33, 40),
src_line: 2,
src_col: 3,
gen_col: 5,
}),
None,
],
)
}
#[cfg(test)]
fn message_at(line: usize, col: usize) -> RustcMessage {
RustcMessage {
path: "p.vrs".to_string(),
line,
col,
level: "error".to_string(),
code: Some("E0277".to_string()),
message: "cannot add `&str` to `i32`".to_string(),
}
}
#[cfg(test)]
fn run(m: &RustcMessage) -> voltaic_diag::Diagnostic {
remap(m, &map_of(), "p.vlt", VLT, "p.vrs", VRS)
}
pub(crate) fn char_offset(source: &str, line: usize, col: usize) -> Option<usize> {
if line == 0 || col == 0 {
return None;
}
let mut offset = 0;
for pair in source.lines().enumerate() {
let n = pair.0;
let text = pair.1;
if n + 1 == line {
if text.chars().count() < col - 1 {
return None;
}
let within: usize = text.chars().take(col - 1).map(char::len_utf8).sum();
return Some(offset + within);
}
offset += text.len() + 1;
}
None
}
#[cfg(test)]
const SRC_CHAR_OFFSET: &str = "ab\n cd\n";
pub(crate) fn generated_offset(source: &str, line: usize, col: usize) -> usize {
char_offset(source, line, col).unwrap_or_else(|| source.len().saturating_sub(1))
}
#[cfg(test)]
const SRC_GENERATED_OFFSET: &str = "fn main() {\n}\n";
pub(crate) fn narrow(span: Span, source: &str, line: usize, col: usize) -> Span {
let Some(offset) = char_offset(source, line, col) else {
return span;
};
if offset < span.0 || offset >= span.1 {
return span;
}
let n = source.len();
let width = source[offset..n].chars().next().map_or(1, char::len_utf8);
(offset, offset + width)
}
#[cfg(test)]
const SRC_NARROW: &str = "add = (a: i32, b: i32): i32 ->\n a + \"x\"\n";
#[cfg(test)]
const SPAN: (usize, usize) = (33, 40);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RustcMessage {
pub path: String,
pub line: usize,
pub col: usize,
pub level: String,
pub code: Option<String>,
pub message: String,
}
impl RustcMessage {
pub fn parse(line: &str) -> Option<Self> {
let head = line.split_once(": ")?;
let position = head.0;
let rest = head.1;
let tail = position.rsplit_once(':')?;
let col: usize = tail.1.parse().ok()?;
let stem = tail.0.rsplit_once(':')?;
let line_no: usize = stem.1.parse().ok()?;
let labelled = rest.split_once(": ")?;
let label = labelled.0;
let message = labelled.1;
let bracketed = label.split_once('[');
let level = bracketed.as_ref().map_or(label, |parts| parts.0);
let code = bracketed
.as_ref()
.map(|parts| parts.1.trim_end_matches(']').to_string());
if level != "error" && level != "warning" {
return None;
}
Some(RustcMessage {
path: stem.0.to_string(),
line: line_no,
col,
level: level.to_string(),
code,
message: message.to_string(),
})
}
pub fn is_error(&self) -> bool {
self.level == "error"
}
}
#[cfg(test)]
mod __vlt_tests {
use super::*;
#[test]
fn narrows_to_the_exact_character_on_a_verbatim_line() {
let d = run(&message_at(2, 7));
assert_eq!(d.path, "p.vlt");
assert_eq!(&VLT[d.span.0..d.span.1], "+");
assert_eq!(d.code.text(), "E0277");
assert_eq!(
d.message, "cannot add `&str` to `i32`",
"rustc's message passes through"
);
assert!(d.help.is_none());
}
#[test]
fn underlines_the_whole_span_when_the_column_misses() {
let d = run(&message_at(2, 99));
assert_eq!(d.span, (33, 40));
assert_eq!(&VLT[d.span.0..d.span.1], "a + \"x\"");
}
#[test]
fn reports_a_synthetic_line_against_the_generated_file() {
let d = run(&message_at(3, 1));
assert_eq!(d.path, "p.vrs");
assert_eq!(d.source, VRS);
let help = d.help.expect("a synthetic line should explain itself");
assert!(help.contains("Voltaic generated"), "{}", help);
assert!(help.contains("p.vrs:3:1"), "{}", help);
}
#[test]
fn reports_an_unmapped_line_against_the_generated_file() {
let d = run(&message_at(99, 1));
assert_eq!(d.path, "p.vrs");
}
#[test]
fn falls_back_to_the_level_when_rustc_gave_no_code() {
let mut m = message_at(2, 7);
m.code = None;
m.level = "warning".to_string();
assert_eq!(run(&m).code.text(), "warning");
}
#[test]
fn resolves_a_position() {
assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 1), Some(0));
assert_eq!(char_offset(SRC_CHAR_OFFSET, 2, 3), Some(5));
assert_eq!(&SRC_CHAR_OFFSET[5..7], "cd");
}
#[test]
fn allows_the_position_just_past_a_line() {
assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 3), Some(2));
}
#[test]
fn declines_a_column_past_the_line() {
assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 9), None);
}
#[test]
fn declines_a_line_past_the_text() {
assert_eq!(char_offset(SRC_CHAR_OFFSET, 9, 1), None);
}
#[test]
fn declines_a_zero_line_or_column() {
assert_eq!(char_offset(SRC_CHAR_OFFSET, 0, 1), None);
assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 0), None);
}
#[test]
fn counts_characters_and_returns_bytes() {
assert_eq!(char_offset("é x\n", 1, 2), Some(2));
assert_eq!(&"é x\n"[2..3], " ");
}
#[test]
fn resolves_a_real_position() {
assert_eq!(generated_offset(SRC_GENERATED_OFFSET, 2, 1), 12);
assert_eq!(SRC_GENERATED_OFFSET[12..13].chars().next(), Some('}'));
}
#[test]
fn clamps_a_position_past_the_end() {
assert_eq!(
generated_offset(SRC_GENERATED_OFFSET, 99, 99),
SRC_GENERATED_OFFSET.len() - 1
);
}
#[test]
fn survives_an_empty_source() {
assert_eq!(generated_offset("", 1, 1), 0);
}
#[test]
fn narrows_to_one_character_inside_the_span() {
let got = narrow(SPAN, SRC_NARROW, 2, 5);
assert_eq!(&SRC_NARROW[got.0..got.1], "+");
}
#[test]
fn keeps_the_span_when_the_column_is_past_the_line() {
assert_eq!(narrow(SPAN, SRC_NARROW, 2, 99), SPAN);
}
#[test]
fn keeps_the_span_when_the_position_is_elsewhere() {
assert_eq!(narrow(SPAN, SRC_NARROW, 1, 1), SPAN);
}
#[test]
fn keeps_the_span_when_the_line_does_not_exist() {
assert_eq!(narrow(SPAN, SRC_NARROW, 99, 1), SPAN);
}
#[test]
fn narrows_to_a_whole_multibyte_character() {
let src = "x : ()\nx =\n é\n";
let span = (13, 15);
assert_eq!(&src[span.0..span.1], "é", "the premise of this test");
let got = narrow(span, src, 3, 3);
assert_eq!(&src[got.0..got.1], "é");
}
#[test]
fn parses_a_coded_error() {
let m = RustcMessage::parse(
".tmp/typeerr.vrs:6:24: error[E0308]: mismatched types: expected `i32`, found `&str`",
)
.expect("should parse");
assert_eq!(m.path, ".tmp/typeerr.vrs");
assert_eq!((m.line, m.col), (6, 24));
assert_eq!(m.level, "error");
assert_eq!(m.code.as_deref(), Some("E0308"));
assert_eq!(
m.message, "mismatched types: expected `i32`, found `&str`",
"the message keeps rustc's own colons"
);
assert!(m.is_error());
}
#[test]
fn keeps_a_message_containing_a_colon() {
let m = RustcMessage::parse(".tmp/tail.vrs:2:7: error[E0277]: cannot add `&str` to `i32`: no implementation for `i32 + &str`").expect("should parse");
assert_eq!(
m.message,
"cannot add `&str` to `i32`: no implementation for `i32 + &str`"
);
}
#[test]
fn parses_an_uncoded_warning() {
let m =
RustcMessage::parse("main.vrs:3:9: warning: unused variable: `x`").expect("should parse");
assert_eq!(m.level, "warning");
assert_eq!(m.code, None);
assert_eq!(m.message, "unused variable: `x`");
assert!(!m.is_error());
}
#[test]
fn declines_the_summary_line() {
assert_eq!(
RustcMessage::parse("error: aborting due to 1 previous error"),
None
);
}
#[test]
fn declines_an_unpositioned_note() {
assert_eq!(
RustcMessage::parse("note: run with `RUST_BACKTRACE=1`"),
None
);
assert_eq!(RustcMessage::parse(""), None);
}
#[test]
fn survives_a_colon_in_the_path() {
let m = RustcMessage::parse("C:\\out\\main.vrs:6:24: error[E0308]: mismatched types")
.expect("should parse");
assert_eq!(m.path, "C:\\out\\main.vrs");
assert_eq!((m.line, m.col), (6, 24));
}
#[test]
fn declines_an_unknown_level() {
assert_eq!(RustcMessage::parse("main.vrs:1:1: help: try this"), None);
}
}