1//! `rustc` and `cargo` diagnostics extractors.
2//!
3//! These parse diagnostics from the respective stderr JSON output using the
4//! data structures defined in [`cargo_metadata::diagnostic`].
5
6use super::{Diagnostics, Level, Message};
7use bstr::ByteSlice;
8use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel, DiagnosticSpan};
9use regex::Regex;
10use std::{
11 path::{Path, PathBuf},
12 sync::OnceLock,
13};
14
15fn diag_line(diag: &Diagnostic, file: &Path) -> Option<(spanned::Span, usize)> {
16 let span: impl Fn(bool) -> Option<(…)> = |primary: bool| {
17 diagIter<'_, DiagnosticSpan>.spans
18 .iter()
19 .find_map(|span: &DiagnosticSpan| span_line(span, file, primary))
20 };
21 span(primary:true).or_else(|| span(primary:false))
22}
23
24/// Put the message and its children into the line-indexed list.
25fn insert_recursive(
26 diag: Diagnostic,
27 file: &Path,
28 messages: &mut Vec<Vec<Message>>,
29 messages_from_unknown_file_or_line: &mut Vec<Message>,
30 line: Option<(spanned::Span, usize)>,
31) {
32 let line = diag_line(&diag, file).or(line);
33 let msg = Message {
34 level: diag.level.into(),
35 message: diag.message,
36 line: line.as_ref().map(|&(_, l)| l),
37 span: line.as_ref().map(|(s, _)| s.clone()),
38 code: diag.code.map(|x| x.code),
39 };
40 if let Some((_, line)) = line.clone() {
41 if messages.len() <= line {
42 messages.resize_with(line + 1, Vec::new);
43 }
44 messages[line].push(msg);
45 // All other messages go into the general bin, unless they are specifically of the
46 // "aborting due to X previous errors" variety, as we never want to match those. They
47 // only count the number of errors and provide no useful information about the tests.
48 } else if !(msg.message.starts_with("aborting due to")
49 && msg.message.contains("previous error"))
50 {
51 messages_from_unknown_file_or_line.push(msg);
52 }
53 for child in diag.children {
54 insert_recursive(
55 child,
56 file,
57 messages,
58 messages_from_unknown_file_or_line,
59 line.clone(),
60 )
61 }
62}
63
64/// Returns the most expanded line number *in the given file*, if possible.
65fn span_line(span: &DiagnosticSpan, file: &Path, primary: bool) -> Option<(spanned::Span, usize)> {
66 let file_name = PathBuf::from(&span.file_name);
67 if let Some(exp) = &span.expansion {
68 if let Some(line) = span_line(&exp.span, file, !primary || span.is_primary) {
69 return Some(line);
70 } else if file_name != file {
71 return if !primary && span.is_primary {
72 span_line(&exp.span, file, false)
73 } else {
74 None
75 };
76 }
77 }
78 ((!primary || span.is_primary) && file_name == file).then(|| {
79 let span = || {
80 Some((
81 spanned::Span {
82 file: file_name,
83 bytes: usize::try_from(span.byte_start).unwrap()
84 ..usize::try_from(span.byte_end).unwrap(),
85 },
86 span.line_start,
87 ))
88 };
89 span().unwrap_or_default()
90 })
91}
92
93fn filter_annotations_from_rendered(rendered: &str) -> std::borrow::Cow<'_, str> {
94 static ANNOTATIONS_RE: OnceLock<Regex> = OnceLock::new();
95 ANNOTATIONS_RE
96 .get_or_init(|| Regex::new(r" *//(\[[a-z,]+\])?~.*").unwrap())
97 .replace_all(haystack:rendered, rep:"")
98}
99
100/// `rustc` diagnostics extractor.
101pub fn rustc_diagnostics_extractor(file: &Path, stderr: &[u8]) -> Diagnostics {
102 let mut rendered = Vec::new();
103 let mut messages = vec![];
104 let mut messages_from_unknown_file_or_line = vec![];
105 for line in stderr.lines_with_terminator() {
106 if line.starts_with_str(b"{") {
107 let msg =
108 serde_json::from_slice::<cargo_metadata::diagnostic::Diagnostic>(line).unwrap();
109
110 rendered.extend(
111 filter_annotations_from_rendered(msg.rendered.as_ref().unwrap()).as_bytes(),
112 );
113 insert_recursive(
114 msg,
115 file,
116 &mut messages,
117 &mut messages_from_unknown_file_or_line,
118 None,
119 );
120 } else {
121 // FIXME: do we want to throw interpreter stderr into a separate file?
122 rendered.extend(line);
123 }
124 }
125 Diagnostics {
126 rendered,
127 messages,
128 messages_from_unknown_file_or_line,
129 }
130}
131
132/// `cargo` diagnostics extractor.
133pub fn cargo_diagnostics_extractor(file: &Path, stderr: &[u8]) -> Diagnostics {
134 let mut rendered = Vec::new();
135 let mut messages = vec![];
136 let mut messages_from_unknown_file_or_line = vec![];
137 for message in cargo_metadata::Message::parse_stream(stderr) {
138 match message.unwrap() {
139 cargo_metadata::Message::CompilerMessage(msg) => {
140 let msg = msg.message;
141 rendered.extend(
142 filter_annotations_from_rendered(msg.rendered.as_ref().unwrap()).as_bytes(),
143 );
144 insert_recursive(
145 msg,
146 file,
147 &mut messages,
148 &mut messages_from_unknown_file_or_line,
149 None,
150 );
151 }
152 cargo_metadata::Message::TextLine(line) => {
153 rendered.extend(line.bytes());
154 rendered.push(b'\n')
155 }
156 _ => {}
157 }
158 }
159 Diagnostics {
160 rendered,
161 messages,
162 messages_from_unknown_file_or_line,
163 }
164}
165
166impl From<DiagnosticLevel> for Level {
167 fn from(value: DiagnosticLevel) -> Self {
168 match value {
169 DiagnosticLevel::Ice => Level::Ice,
170 DiagnosticLevel::Error => Level::Error,
171 DiagnosticLevel::Warning => Level::Warn,
172 DiagnosticLevel::FailureNote => Level::FailureNote,
173 DiagnosticLevel::Note => Level::Note,
174 DiagnosticLevel::Help => Level::Help,
175 other: DiagnosticLevel => panic!("rustc got a new kind of diagnostic level: {other:?}"),
176 }
177 }
178}
179