1 | use bstr::ByteSlice; |
2 | use regex::Regex; |
3 | use std::{ |
4 | num::NonZeroUsize, |
5 | path::{Path, PathBuf}, |
6 | }; |
7 | |
8 | #[derive (serde::Deserialize, Debug)] |
9 | struct 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)] |
18 | pub(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. |
30 | pub 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)] |
38 | struct Expansion { |
39 | span: RustcSpan, |
40 | } |
41 | |
42 | #[derive (serde::Deserialize, Debug)] |
43 | struct 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)] |
52 | pub struct Span { |
53 | pub line_start: NonZeroUsize, |
54 | pub column_start: NonZeroUsize, |
55 | pub line_end: NonZeroUsize, |
56 | pub column_end: NonZeroUsize, |
57 | } |
58 | |
59 | impl 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 | |
84 | impl 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)] |
100 | pub(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 | |
110 | impl 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 | |
149 | impl 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 | |
161 | pub(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 | |
166 | pub(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 | |