Skip to main content

voltaic_driver/
remap.rs

1use voltaic_diag::Diagnostic;
2
3use voltaic_diag::DiagnosticCode;
4
5use voltaic_diag::SourceMap;
6
7use voltaic_diag::Span;
8
9pub fn remap(
10  message: &RustcMessage,
11  map: &SourceMap,
12  vlt_path: &str,
13  vlt_source: &str,
14  vrs_path: &str,
15  vrs_source: &str,
16) -> Diagnostic {
17  let code = DiagnosticCode::Foreign(
18    message
19      .code
20      .clone()
21      .unwrap_or_else(|| message.level.clone()),
22  );
23
24  let severity = if message.level == "warning" {
25    voltaic_diag::Severity::Warning
26  } else {
27    voltaic_diag::Severity::Error
28  };
29
30  match map.origin(message.line) {
31    Some(origin) => {
32      let shifted = origin.map_col(message.col);
33      Diagnostic {
34        code,
35        severity,
36        message: message.message.clone(),
37        span: narrow(origin.span, vlt_source, origin.src_line, shifted),
38        related: Vec::new(),
39        help: None,
40        path: vlt_path.to_string(),
41        source: vlt_source.to_string(),
42      }
43    }
44    None => {
45      let offset = generated_offset(vrs_source, message.line, message.col);
46      let note = format!(
47        "this is on a line Voltaic generated, so it has no place in your source; see {}:{}:{}",
48        vrs_path, message.line, message.col
49      );
50      Diagnostic {
51        code,
52        severity,
53        message: message.message.clone(),
54        span: (offset, offset + 1),
55        related: Vec::new(),
56        help: Some(note),
57        path: vrs_path.to_string(),
58        source: vrs_source.to_string(),
59      }
60    }
61  }
62}
63
64#[cfg(test)]
65const VLT: &str = "add = (a: i32, b: i32): i32 ->\n  a + \"x\"\n";
66
67#[cfg(test)]
68const VRS: &str = "fn add(a: i32, b: i32) -> i32 {\n    a + \"x\"\n}\n";
69
70#[cfg(test)]
71fn map_of() -> SourceMap {
72  SourceMap::new(
73    "p.vlt",
74    vec![
75      Some(voltaic_diag::Origin {
76        span: (0, 30),
77        src_line: 1,
78        src_col: 1,
79        gen_col: 1,
80      }),
81      Some(voltaic_diag::Origin {
82        span: (33, 40),
83        src_line: 2,
84        src_col: 3,
85        gen_col: 5,
86      }),
87      None,
88    ],
89  )
90}
91
92#[cfg(test)]
93fn message_at(line: usize, col: usize) -> RustcMessage {
94  RustcMessage {
95    path: "p.vrs".to_string(),
96    line,
97    col,
98    level: "error".to_string(),
99    code: Some("E0277".to_string()),
100    message: "cannot add `&str` to `i32`".to_string(),
101  }
102}
103
104#[cfg(test)]
105fn run(m: &RustcMessage) -> voltaic_diag::Diagnostic {
106  remap(m, &map_of(), "p.vlt", VLT, "p.vrs", VRS)
107}
108
109pub(crate) fn char_offset(source: &str, line: usize, col: usize) -> Option<usize> {
110  if line == 0 || col == 0 {
111    return None;
112  }
113  let mut offset = 0;
114  for pair in source.lines().enumerate() {
115    let n = pair.0;
116    let text = pair.1;
117    if n + 1 == line {
118      if text.chars().count() < col - 1 {
119        return None;
120      }
121      let within: usize = text.chars().take(col - 1).map(char::len_utf8).sum();
122      return Some(offset + within);
123    }
124    offset += text.len() + 1;
125  }
126  None
127}
128
129#[cfg(test)]
130const SRC_CHAR_OFFSET: &str = "ab\n  cd\n";
131
132pub(crate) fn generated_offset(source: &str, line: usize, col: usize) -> usize {
133  char_offset(source, line, col).unwrap_or_else(|| source.len().saturating_sub(1))
134}
135
136#[cfg(test)]
137const SRC_GENERATED_OFFSET: &str = "fn main() {\n}\n";
138
139pub(crate) fn narrow(span: Span, source: &str, line: usize, col: usize) -> Span {
140  let Some(offset) = char_offset(source, line, col) else {
141    return span;
142  };
143  if offset < span.0 || offset >= span.1 {
144    return span;
145  }
146  let n = source.len();
147  let width = source[offset..n].chars().next().map_or(1, char::len_utf8);
148  (offset, offset + width)
149}
150
151#[cfg(test)]
152const SRC_NARROW: &str = "add = (a: i32, b: i32): i32 ->\n  a + \"x\"\n";
153
154#[cfg(test)]
155const SPAN: (usize, usize) = (33, 40);
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct RustcMessage {
159  pub path: String,
160  pub line: usize,
161  pub col: usize,
162  pub level: String,
163  pub code: Option<String>,
164  pub message: String,
165}
166
167impl RustcMessage {
168  pub fn parse(line: &str) -> Option<Self> {
169    let head = line.split_once(": ")?;
170    let position = head.0;
171    let rest = head.1;
172
173    let tail = position.rsplit_once(':')?;
174    let col: usize = tail.1.parse().ok()?;
175    let stem = tail.0.rsplit_once(':')?;
176    let line_no: usize = stem.1.parse().ok()?;
177
178    let labelled = rest.split_once(": ")?;
179    let label = labelled.0;
180    let message = labelled.1;
181    let bracketed = label.split_once('[');
182    let level = bracketed.as_ref().map_or(label, |parts| parts.0);
183    let code = bracketed
184      .as_ref()
185      .map(|parts| parts.1.trim_end_matches(']').to_string());
186    if level != "error" && level != "warning" {
187      return None;
188    }
189
190    Some(RustcMessage {
191      path: stem.0.to_string(),
192      line: line_no,
193      col,
194      level: level.to_string(),
195      code,
196      message: message.to_string(),
197    })
198  }
199
200  pub fn is_error(&self) -> bool {
201    self.level == "error"
202  }
203}
204
205#[cfg(test)]
206mod __vlt_tests {
207  use super::*;
208
209  #[test]
210  fn narrows_to_the_exact_character_on_a_verbatim_line() {
211    let d = run(&message_at(2, 7));
212    assert_eq!(d.path, "p.vlt");
213    assert_eq!(&VLT[d.span.0..d.span.1], "+");
214    assert_eq!(d.code.text(), "E0277");
215    assert_eq!(
216      d.message, "cannot add `&str` to `i32`",
217      "rustc's message passes through"
218    );
219    assert!(d.help.is_none());
220  }
221
222  #[test]
223  fn underlines_the_whole_span_when_the_column_misses() {
224    let d = run(&message_at(2, 99));
225    assert_eq!(d.span, (33, 40));
226    assert_eq!(&VLT[d.span.0..d.span.1], "a + \"x\"");
227  }
228
229  #[test]
230  fn reports_a_synthetic_line_against_the_generated_file() {
231    let d = run(&message_at(3, 1));
232    assert_eq!(d.path, "p.vrs");
233    assert_eq!(d.source, VRS);
234    let help = d.help.expect("a synthetic line should explain itself");
235    assert!(help.contains("Voltaic generated"), "{}", help);
236    assert!(help.contains("p.vrs:3:1"), "{}", help);
237  }
238
239  #[test]
240  fn reports_an_unmapped_line_against_the_generated_file() {
241    let d = run(&message_at(99, 1));
242    assert_eq!(d.path, "p.vrs");
243  }
244
245  #[test]
246  fn falls_back_to_the_level_when_rustc_gave_no_code() {
247    let mut m = message_at(2, 7);
248    m.code = None;
249    m.level = "warning".to_string();
250    assert_eq!(run(&m).code.text(), "warning");
251  }
252
253  #[test]
254  fn resolves_a_position() {
255    assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 1), Some(0));
256    assert_eq!(char_offset(SRC_CHAR_OFFSET, 2, 3), Some(5));
257    assert_eq!(&SRC_CHAR_OFFSET[5..7], "cd");
258  }
259
260  #[test]
261  fn allows_the_position_just_past_a_line() {
262    assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 3), Some(2));
263  }
264
265  #[test]
266  fn declines_a_column_past_the_line() {
267    assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 9), None);
268  }
269
270  #[test]
271  fn declines_a_line_past_the_text() {
272    assert_eq!(char_offset(SRC_CHAR_OFFSET, 9, 1), None);
273  }
274
275  #[test]
276  fn declines_a_zero_line_or_column() {
277    assert_eq!(char_offset(SRC_CHAR_OFFSET, 0, 1), None);
278    assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 0), None);
279  }
280
281  #[test]
282  fn counts_characters_and_returns_bytes() {
283    assert_eq!(char_offset("é x\n", 1, 2), Some(2));
284    assert_eq!(&"é x\n"[2..3], " ");
285  }
286
287  #[test]
288  fn resolves_a_real_position() {
289    assert_eq!(generated_offset(SRC_GENERATED_OFFSET, 2, 1), 12);
290    assert_eq!(SRC_GENERATED_OFFSET[12..13].chars().next(), Some('}'));
291  }
292
293  #[test]
294  fn clamps_a_position_past_the_end() {
295    assert_eq!(
296      generated_offset(SRC_GENERATED_OFFSET, 99, 99),
297      SRC_GENERATED_OFFSET.len() - 1
298    );
299  }
300
301  #[test]
302  fn survives_an_empty_source() {
303    assert_eq!(generated_offset("", 1, 1), 0);
304  }
305
306  #[test]
307  fn narrows_to_one_character_inside_the_span() {
308    let got = narrow(SPAN, SRC_NARROW, 2, 5);
309    assert_eq!(&SRC_NARROW[got.0..got.1], "+");
310  }
311
312  #[test]
313  fn keeps_the_span_when_the_column_is_past_the_line() {
314    assert_eq!(narrow(SPAN, SRC_NARROW, 2, 99), SPAN);
315  }
316
317  #[test]
318  fn keeps_the_span_when_the_position_is_elsewhere() {
319    assert_eq!(narrow(SPAN, SRC_NARROW, 1, 1), SPAN);
320  }
321
322  #[test]
323  fn keeps_the_span_when_the_line_does_not_exist() {
324    assert_eq!(narrow(SPAN, SRC_NARROW, 99, 1), SPAN);
325  }
326
327  #[test]
328  fn narrows_to_a_whole_multibyte_character() {
329    let src = "x : ()\nx =\n  é\n";
330    let span = (13, 15);
331    assert_eq!(&src[span.0..span.1], "é", "the premise of this test");
332    let got = narrow(span, src, 3, 3);
333    assert_eq!(&src[got.0..got.1], "é");
334  }
335
336  #[test]
337  fn parses_a_coded_error() {
338    let m = RustcMessage::parse(
339      ".tmp/typeerr.vrs:6:24: error[E0308]: mismatched types: expected `i32`, found `&str`",
340    )
341    .expect("should parse");
342    assert_eq!(m.path, ".tmp/typeerr.vrs");
343    assert_eq!((m.line, m.col), (6, 24));
344    assert_eq!(m.level, "error");
345    assert_eq!(m.code.as_deref(), Some("E0308"));
346    assert_eq!(
347      m.message, "mismatched types: expected `i32`, found `&str`",
348      "the message keeps rustc's own colons"
349    );
350    assert!(m.is_error());
351  }
352
353  #[test]
354  fn keeps_a_message_containing_a_colon() {
355    let m = RustcMessage::parse(".tmp/tail.vrs:2:7: error[E0277]: cannot add `&str` to `i32`: no implementation for `i32 + &str`").expect("should parse");
356    assert_eq!(
357      m.message,
358      "cannot add `&str` to `i32`: no implementation for `i32 + &str`"
359    );
360  }
361
362  #[test]
363  fn parses_an_uncoded_warning() {
364    let m =
365      RustcMessage::parse("main.vrs:3:9: warning: unused variable: `x`").expect("should parse");
366    assert_eq!(m.level, "warning");
367    assert_eq!(m.code, None);
368    assert_eq!(m.message, "unused variable: `x`");
369    assert!(!m.is_error());
370  }
371
372  #[test]
373  fn declines_the_summary_line() {
374    assert_eq!(
375      RustcMessage::parse("error: aborting due to 1 previous error"),
376      None
377    );
378  }
379
380  #[test]
381  fn declines_an_unpositioned_note() {
382    assert_eq!(
383      RustcMessage::parse("note: run with `RUST_BACKTRACE=1`"),
384      None
385    );
386    assert_eq!(RustcMessage::parse(""), None);
387  }
388
389  #[test]
390  fn survives_a_colon_in_the_path() {
391    let m = RustcMessage::parse("C:\\out\\main.vrs:6:24: error[E0308]: mismatched types")
392      .expect("should parse");
393    assert_eq!(m.path, "C:\\out\\main.vrs");
394    assert_eq!((m.line, m.col), (6, 24));
395  }
396
397  #[test]
398  fn declines_an_unknown_level() {
399    assert_eq!(RustcMessage::parse("main.vrs:1:1: help: try this"), None);
400  }
401}