1use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::sync::OnceLock;
6
7use regex::Regex;
8use serde::Deserialize;
9
10use crate::errors::{Error, ErrorKind};
11use crate::runtest::ProcRes;
12
13#[derive(Deserialize)]
14struct Diagnostic {
15 message: String,
16 code: Option<DiagnosticCode>,
17 level: String,
18 spans: Vec<DiagnosticSpan>,
19 children: Vec<Diagnostic>,
20 rendered: Option<String>,
21}
22
23#[derive(Deserialize)]
24struct ArtifactNotification {
25 #[allow(dead_code)]
26 artifact: PathBuf,
27}
28
29#[derive(Deserialize)]
30struct UnusedExternNotification {
31 #[allow(dead_code)]
32 lint_level: String,
33 #[allow(dead_code)]
34 unused_extern_names: Vec<String>,
35}
36
37#[derive(Deserialize, Clone)]
38struct DiagnosticSpan {
39 file_name: String,
40 line_start: usize,
41 line_end: usize,
42 column_start: usize,
43 column_end: usize,
44 is_primary: bool,
45 label: Option<String>,
46 suggested_replacement: Option<String>,
47 expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
48}
49
50#[derive(Deserialize)]
51struct FutureIncompatReport {
52 future_incompat_report: Vec<FutureBreakageItem>,
53}
54
55#[derive(Deserialize)]
56struct FutureBreakageItem {
57 diagnostic: Diagnostic,
58}
59
60impl DiagnosticSpan {
61 fn first_callsite_in_file(&self, file_name: &str) -> &DiagnosticSpan {
64 if self.file_name == file_name {
65 self
66 } else {
67 self.expansion
68 .as_ref()
69 .map(|origin| origin.span.first_callsite_in_file(file_name))
70 .unwrap_or(self)
71 }
72 }
73}
74
75#[derive(Deserialize, Clone)]
76struct DiagnosticSpanMacroExpansion {
77 span: DiagnosticSpan,
79
80 macro_decl_name: String,
82}
83
84#[derive(Deserialize, Clone)]
85struct DiagnosticCode {
86 code: String,
88}
89
90pub fn rustfix_diagnostics_only(output: &str) -> String {
91 output
92 .lines()
93 .filter(|line| line.starts_with('{') && serde_json::from_str::<Diagnostic>(line).is_ok())
94 .collect()
95}
96
97pub fn extract_rendered(output: &str) -> String {
98 output
99 .lines()
100 .filter_map(|line| {
101 if line.starts_with('{') {
102 if let Ok(diagnostic) = serde_json::from_str::<Diagnostic>(line) {
103 diagnostic.rendered
104 } else if let Ok(report) = serde_json::from_str::<FutureIncompatReport>(line) {
105 if report.future_incompat_report.is_empty() {
106 None
107 } else {
108 Some(format!(
109 "Future incompatibility report: {}",
110 report
111 .future_incompat_report
112 .into_iter()
113 .map(|item| {
114 format!(
115 "Future breakage diagnostic:\n{}",
116 item.diagnostic
117 .rendered
118 .unwrap_or_else(|| "Not rendered".to_string())
119 )
120 })
121 .collect::<String>()
122 ))
123 }
124 } else if serde_json::from_str::<ArtifactNotification>(line).is_ok() {
125 None
127 } else if serde_json::from_str::<UnusedExternNotification>(line).is_ok() {
128 None
130 } else {
131 Some(format!("{line}\n"))
135 }
136 } else {
137 Some(format!("{}\n", line))
139 }
140 })
141 .collect()
142}
143
144pub fn parse_output(file_name: &str, output: &str, proc_res: &ProcRes) -> Vec<Error> {
145 output.lines().flat_map(|line| parse_line(file_name, line, output, proc_res)).collect()
146}
147
148fn parse_line(file_name: &str, line: &str, output: &str, proc_res: &ProcRes) -> Vec<Error> {
149 if line.starts_with('{') {
152 match serde_json::from_str::<Diagnostic>(line) {
153 Ok(diagnostic) => {
154 let mut expected_errors = vec![];
155 push_expected_errors(&mut expected_errors, &diagnostic, &[], file_name);
156 expected_errors
157 }
158 Err(error) => {
159 if serde_json::from_str::<FutureIncompatReport>(line).is_ok() {
162 vec![]
163 } else {
164 proc_res.fatal(
165 Some(&format!(
166 "failed to decode compiler output as json: \
167 `{}`\nline: {}\noutput: {}",
168 error, line, output
169 )),
170 || (),
171 );
172 }
173 }
174 }
175 } else {
176 vec![]
177 }
178}
179
180fn push_expected_errors(
181 expected_errors: &mut Vec<Error>,
182 diagnostic: &Diagnostic,
183 default_spans: &[&DiagnosticSpan],
184 file_name: &str,
185) {
186 let spans_info_in_this_file: Vec<_> = diagnostic
188 .spans
189 .iter()
190 .map(|span| (span.is_primary, span.first_callsite_in_file(file_name)))
191 .filter(|(_, span)| Path::new(&span.file_name) == Path::new(&file_name))
192 .collect();
193
194 let spans_in_this_file: Vec<_> = spans_info_in_this_file.iter().map(|(_, span)| span).collect();
195
196 let primary_spans: Vec<_> = spans_info_in_this_file
197 .iter()
198 .filter(|(is_primary, _)| *is_primary)
199 .map(|(_, span)| span)
200 .take(1) .cloned()
202 .collect();
203 let primary_spans = if primary_spans.is_empty() {
204 default_spans
207 } else {
208 &primary_spans
209 };
210
211 let with_code = |span: Option<&DiagnosticSpan>, text: &str| {
219 let span_str = match span {
228 Some(DiagnosticSpan { line_start, column_start, line_end, column_end, .. }) => {
229 format!("{line_start}:{column_start}: {line_end}:{column_end}")
230 }
231 None => format!("?:?: ?:?"),
232 };
233 match &diagnostic.code {
234 Some(code) => format!("{span_str}: {text} [{}]", code.code),
235 None => format!("{span_str}: {text}"),
236 }
237 };
238
239 let mut message_lines = diagnostic.message.lines();
243 if let Some(first_line) = message_lines.next() {
244 let ignore = |s| {
245 static RE: OnceLock<Regex> = OnceLock::new();
246 RE.get_or_init(|| {
247 Regex::new(r"aborting due to \d+ previous errors?|\d+ warnings? emitted").unwrap()
248 })
249 .is_match(s)
250 };
251
252 if primary_spans.is_empty() && !ignore(first_line) {
253 let msg = with_code(None, first_line);
254 let kind = ErrorKind::from_str(&diagnostic.level).ok();
255 expected_errors.push(Error { line_num: None, kind, msg });
256 } else {
257 for span in primary_spans {
258 let msg = with_code(Some(span), first_line);
259 let kind = ErrorKind::from_str(&diagnostic.level).ok();
260 expected_errors.push(Error { line_num: Some(span.line_start), kind, msg });
261 }
262 }
263 }
264 for next_line in message_lines {
265 if primary_spans.is_empty() {
266 expected_errors.push(Error {
267 line_num: None,
268 kind: None,
269 msg: with_code(None, next_line),
270 });
271 } else {
272 for span in primary_spans {
273 expected_errors.push(Error {
274 line_num: Some(span.line_start),
275 kind: None,
276 msg: with_code(Some(span), next_line),
277 });
278 }
279 }
280 }
281
282 for span in primary_spans {
284 if let Some(ref suggested_replacement) = span.suggested_replacement {
285 for (index, line) in suggested_replacement.lines().enumerate() {
286 expected_errors.push(Error {
287 line_num: Some(span.line_start + index),
288 kind: Some(ErrorKind::Suggestion),
289 msg: line.to_string(),
290 });
291 }
292 }
293 }
294
295 for span in primary_spans {
297 if let Some(frame) = &span.expansion {
298 push_backtrace(expected_errors, frame, file_name);
299 }
300 }
301
302 for span in spans_in_this_file.iter().filter(|span| span.label.is_some()) {
304 expected_errors.push(Error {
305 line_num: Some(span.line_start),
306 kind: Some(ErrorKind::Note),
307 msg: span.label.clone().unwrap(),
308 });
309 }
310
311 for child in &diagnostic.children {
313 push_expected_errors(expected_errors, child, primary_spans, file_name);
314 }
315}
316
317fn push_backtrace(
318 expected_errors: &mut Vec<Error>,
319 expansion: &DiagnosticSpanMacroExpansion,
320 file_name: &str,
321) {
322 if Path::new(&expansion.span.file_name) == Path::new(&file_name) {
323 expected_errors.push(Error {
324 line_num: Some(expansion.span.line_start),
325 kind: Some(ErrorKind::Note),
326 msg: format!("in this expansion of {}", expansion.macro_decl_name),
327 });
328 }
329
330 if let Some(previous_expansion) = &expansion.span.expansion {
331 push_backtrace(expected_errors, previous_expansion, file_name);
332 }
333}