| use bstr::ByteSlice; |
| use regex::Regex; |
| use std::{ |
| num::NonZeroUsize, |
| path::{Path, PathBuf}, |
| }; |
| |
| #[derive(serde::Deserialize, Debug)] |
| struct RustcDiagnosticCode { |
| code: String, |
| } |
| |
| #[derive(serde::Deserialize, Debug)] |
| struct RustcMessage { |
| rendered: Option<String>, |
| spans: Vec<RustcSpan>, |
| level: String, |
| message: String, |
| children: Vec<RustcMessage>, |
| code: Option<RustcDiagnosticCode>, |
| } |
| |
| #[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] |
| /// The different levels of diagnostic messages and their relative ranking. |
| pub enum Level { |
| /// internal compiler errors |
| Ice = 5, |
| /// ´error´ level messages |
| Error = 4, |
| /// ´warn´ level messages |
| Warn = 3, |
| /// ´help´ level messages |
| Help = 2, |
| /// ´note´ level messages |
| Note = 1, |
| /// Only used for "For more information about this error, try `rustc --explain EXXXX`". |
| FailureNote = 0, |
| } |
| |
| #[derive(Debug)] |
| /// A diagnostic message. |
| pub struct Message { |
| pub(crate) level: Level, |
| pub(crate) message: String, |
| pub(crate) line_col: Option<spanned::Span>, |
| pub(crate) code: Option<String>, |
| } |
| |
| /// Information about macro expansion. |
| #[derive(serde::Deserialize, Debug)] |
| struct Expansion { |
| span: RustcSpan, |
| } |
| |
| #[derive(serde::Deserialize, Debug)] |
| struct RustcSpan { |
| #[serde(flatten)] |
| line_col: Span, |
| file_name: PathBuf, |
| is_primary: bool, |
| expansion: Option<Box<Expansion>>, |
| } |
| |
| #[derive(serde::Deserialize, Debug, Copy, Clone)] |
| struct Span { |
| line_start: NonZeroUsize, |
| column_start: NonZeroUsize, |
| line_end: NonZeroUsize, |
| column_end: NonZeroUsize, |
| } |
| |
| impl std::str::FromStr for Level { |
| type Err = String; |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| match s { |
| "ERROR" | "error" => Ok(Self::Error), |
| "WARN" | "warning" => Ok(Self::Warn), |
| "HELP" | "help" => Ok(Self::Help), |
| "NOTE" | "note" => Ok(Self::Note), |
| "failure-note" => Ok(Self::FailureNote), |
| "error: internal compiler error" => Ok(Self::Ice), |
| _ => Err(format!("unknown level `{s}`")), |
| } |
| } |
| } |
| |
| #[derive(Debug)] |
| pub(crate) struct Diagnostics { |
| /// Rendered and concatenated version of all diagnostics. |
| /// This is equivalent to non-json diagnostics. |
| pub rendered: Vec<u8>, |
| /// Per line, a list of messages for that line. |
| pub messages: Vec<Vec<Message>>, |
| /// Messages not on any line (usually because they are from libstd) |
| pub messages_from_unknown_file_or_line: Vec<Message>, |
| } |
| |
| impl RustcMessage { |
| fn line(&self, file: &Path) -> Option<spanned::Span> { |
| let span = |primary| self.spans.iter().find_map(|span| span.line(file, primary)); |
| span(true).or_else(|| span(false)) |
| } |
| |
| /// Put the message and its children into the line-indexed list. |
| fn insert_recursive( |
| self, |
| file: &Path, |
| messages: &mut Vec<Vec<Message>>, |
| messages_from_unknown_file_or_line: &mut Vec<Message>, |
| line: Option<spanned::Span>, |
| ) { |
| let line = self.line(file).or(line); |
| let msg = Message { |
| level: self.level.parse().unwrap(), |
| message: self.message, |
| line_col: line.clone(), |
| code: self.code.map(|x| x.code), |
| }; |
| if let Some(line) = line.clone() { |
| if messages.len() <= line.line_start.get() { |
| messages.resize_with(line.line_start.get() + 1, Vec::new); |
| } |
| messages[line.line_start.get()].push(msg); |
| // All other messages go into the general bin, unless they are specifically of the |
| // "aborting due to X previous errors" variety, as we never want to match those. They |
| // only count the number of errors and provide no useful information about the tests. |
| } else if !(msg.message.starts_with("aborting due to") |
| && msg.message.contains("previous error")) |
| { |
| messages_from_unknown_file_or_line.push(msg); |
| } |
| for child in self.children { |
| child.insert_recursive( |
| file, |
| messages, |
| messages_from_unknown_file_or_line, |
| line.clone(), |
| ) |
| } |
| } |
| } |
| |
| impl RustcSpan { |
| /// Returns the most expanded line number *in the given file*, if possible. |
| fn line(&self, file: &Path, primary: bool) -> Option<spanned::Span> { |
| if let Some(exp) = &self.expansion { |
| if let Some(line) = exp.span.line(file, !primary || self.is_primary) { |
| return Some(line); |
| } else if self.file_name != file { |
| return if !primary && self.is_primary { |
| exp.span.line(file, false) |
| } else { |
| None |
| }; |
| } |
| } |
| ((!primary || self.is_primary) && self.file_name == file).then_some(spanned::Span { |
| file: self.file_name.clone(), |
| line_start: self.line_col.line_start, |
| line_end: self.line_col.line_end, |
| col_start: self.line_col.column_start, |
| col_end: self.line_col.column_end, |
| }) |
| } |
| } |
| |
| pub(crate) fn filter_annotations_from_rendered(rendered: &str) -> std::borrow::Cow<'_, str> { |
| let annotations = Regex::new(r" *//(\[[a-z,]+\])?~.*").unwrap(); |
| annotations.replace_all(rendered, "") |
| } |
| |
| pub(crate) fn process(file: &Path, stderr: &[u8]) -> Diagnostics { |
| let mut rendered = Vec::new(); |
| let mut messages = vec![]; |
| let mut messages_from_unknown_file_or_line = vec![]; |
| for (line_number, line) in stderr.lines_with_terminator().enumerate() { |
| if line.starts_with_str(b"{") { |
| match serde_json::from_slice::<RustcMessage>(line) { |
| Ok(msg) => { |
| rendered.extend( |
| filter_annotations_from_rendered(msg.rendered.as_ref().unwrap()).as_bytes(), |
| ); |
| msg.insert_recursive( |
| file, |
| &mut messages, |
| &mut messages_from_unknown_file_or_line, |
| None, |
| ); |
| } |
| Err(err) => { |
| panic!( |
| "failed to parse rustc JSON output at line {line_number}: {err}: {}", |
| line.to_str_lossy() |
| ) |
| } |
| } |
| } else { |
| // FIXME: do we want to throw interpreter stderr into a separate file? |
| rendered.extend(line); |
| } |
| } |
| Diagnostics { |
| rendered, |
| messages, |
| messages_from_unknown_file_or_line, |
| } |
| } |