1use bstr::ByteSlice;
2use regex::Regex;
3use std::{
4 num::NonZeroUsize,
5 path::{Path, PathBuf},
6};
7
8#[derive(serde::Deserialize, Debug)]
9struct RustcMessage {
10 rendered: Option<String>,
11 spans: Vec<RustcSpan>,
12 level: String,
13 message: String,
14 children: Vec<RustcMessage>,
15}
16
17#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
18pub(crate) enum Level {
19 Ice = 5,
20 Error = 4,
21 Warn = 3,
22 Help = 2,
23 Note = 1,
24 /// Only used for "For more information about this error, try `rustc --explain EXXXX`".
25 FailureNote = 0,
26}
27
28#[derive(Debug)]
29/// A diagnostic message.
30pub struct Message {
31 pub(crate) level: Level,
32 pub(crate) message: String,
33 pub(crate) line_col: Option<Span>,
34}
35
36/// Information about macro expansion.
37#[derive(serde::Deserialize, Debug)]
38struct Expansion {
39 span: RustcSpan,
40}
41
42#[derive(serde::Deserialize, Debug)]
43struct RustcSpan {
44 #[serde(flatten)]
45 line_col: Span,
46 file_name: PathBuf,
47 is_primary: bool,
48 expansion: Option<Box<Expansion>>,
49}
50
51#[derive(serde::Deserialize, Debug, Copy, Clone)]
52pub struct Span {
53 pub line_start: NonZeroUsize,
54 pub column_start: NonZeroUsize,
55 pub line_end: NonZeroUsize,
56 pub column_end: NonZeroUsize,
57}
58
59impl Span {
60 pub const INVALID: Self = Self {
61 line_start: NonZeroUsize::MAX,
62 column_start: NonZeroUsize::MAX,
63 line_end: NonZeroUsize::MAX,
64 column_end: NonZeroUsize::MAX,
65 };
66
67 pub fn shrink_to_end(self) -> Span {
68 Self {
69 line_start: self.line_end,
70 column_start: self.column_end,
71 ..self
72 }
73 }
74
75 pub fn shrink_to_start(self) -> Span {
76 Self {
77 line_end: self.line_start,
78 column_end: self.column_start,
79 ..self
80 }
81 }
82}
83
84impl std::str::FromStr for Level {
85 type Err = String;
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 match s {
88 "ERROR" | "error" => Ok(Self::Error),
89 "WARN" | "warning" => Ok(Self::Warn),
90 "HELP" | "help" => Ok(Self::Help),
91 "NOTE" | "note" => Ok(Self::Note),
92 "failure-note" => Ok(Self::FailureNote),
93 "error: internal compiler error" => Ok(Self::Ice),
94 _ => Err(format!("unknown level `{s}`")),
95 }
96 }
97}
98
99#[derive(Debug)]
100pub(crate) struct Diagnostics {
101 /// Rendered and concatenated version of all diagnostics.
102 /// This is equivalent to non-json diagnostics.
103 pub rendered: Vec<u8>,
104 /// Per line, a list of messages for that line.
105 pub messages: Vec<Vec<Message>>,
106 /// Messages not on any line (usually because they are from libstd)
107 pub messages_from_unknown_file_or_line: Vec<Message>,
108}
109
110impl RustcMessage {
111 fn line(&self, file: &Path) -> Option<Span> {
112 let span = |primary| self.spans.iter().find_map(|span| span.line(file, primary));
113 span(true).or_else(|| span(false))
114 }
115
116 /// Put the message and its children into the line-indexed list.
117 fn insert_recursive(
118 self,
119 file: &Path,
120 messages: &mut Vec<Vec<Message>>,
121 messages_from_unknown_file_or_line: &mut Vec<Message>,
122 line: Option<Span>,
123 ) {
124 let line = self.line(file).or(line);
125 let msg = Message {
126 level: self.level.parse().unwrap(),
127 message: self.message,
128 line_col: line,
129 };
130 if let Some(line) = line {
131 if messages.len() <= line.line_start.get() {
132 messages.resize_with(line.line_start.get() + 1, Vec::new);
133 }
134 messages[line.line_start.get()].push(msg);
135 // All other messages go into the general bin, unless they are specifically of the
136 // "aborting due to X previous errors" variety, as we never want to match those. They
137 // only count the number of errors and provide no useful information about the tests.
138 } else if !(msg.message.starts_with("aborting due to")
139 && msg.message.contains("previous error"))
140 {
141 messages_from_unknown_file_or_line.push(msg);
142 }
143 for child in self.children {
144 child.insert_recursive(file, messages, messages_from_unknown_file_or_line, line)
145 }
146 }
147}
148
149impl RustcSpan {
150 /// Returns the most expanded line number *in the given file*, if possible.
151 fn line(&self, file: &Path, primary: bool) -> Option<Span> {
152 if let Some(exp: &Box) = &self.expansion {
153 if let Some(line: Span) = exp.span.line(file, primary:primary && !self.is_primary) {
154 return Some(line);
155 }
156 }
157 ((!primary || self.is_primary) && self.file_name == file).then_some(self.line_col)
158 }
159}
160
161pub(crate) fn filter_annotations_from_rendered(rendered: &str) -> std::borrow::Cow<'_, str> {
162 let annotations: Regex = Regex::new(re:r" *//(\[[a-z,]+\])?~.*").unwrap();
163 annotations.replace_all(text:rendered, rep:"")
164}
165
166pub(crate) fn process(file: &Path, stderr: &[u8]) -> Diagnostics {
167 let mut rendered = Vec::new();
168 let mut messages = vec![];
169 let mut messages_from_unknown_file_or_line = vec![];
170 for (line_number, line) in stderr.lines_with_terminator().enumerate() {
171 if line.starts_with_str(b"{") {
172 match serde_json::from_slice::<RustcMessage>(line) {
173 Ok(msg) => {
174 rendered.extend(
175 filter_annotations_from_rendered(msg.rendered.as_ref().unwrap()).as_bytes(),
176 );
177 msg.insert_recursive(
178 file,
179 &mut messages,
180 &mut messages_from_unknown_file_or_line,
181 None,
182 );
183 }
184 Err(err) => {
185 panic!(
186 "failed to parse rustc JSON output at line {line_number}: {err}: {}",
187 line.to_str_lossy()
188 )
189 }
190 }
191 } else {
192 // FIXME: do we want to throw interpreter stderr into a separate file?
193 rendered.extend(line);
194 }
195 }
196 Diagnostics {
197 rendered,
198 messages,
199 messages_from_unknown_file_or_line,
200 }
201}
202