1use std::path::{Path, PathBuf};
4use std::sync::OnceLock;
5
6use regex::Regex;
7use serde::Deserialize;
8
9use crate::errors::{Error, ErrorKind};
10
11#[derive(Deserialize)]
12struct Diagnostic {
13 message: String,
14 code: Option<DiagnosticCode>,
15 level: String,
16 spans: Vec<DiagnosticSpan>,
17 children: Vec<Diagnostic>,
18 rendered: Option<String>,
19}
20
21#[derive(Deserialize)]
22struct ArtifactNotification {
23 #[allow(dead_code)]
24 artifact: PathBuf,
25}
26
27#[derive(Deserialize)]
28struct UnusedExternNotification {
29 #[allow(dead_code)]
30 lint_level: String,
31 #[allow(dead_code)]
32 unused_extern_names: Vec<String>,
33}
34
35#[derive(Deserialize, Clone)]
36struct DiagnosticSpan {
37 file_name: String,
38 line_start: usize,
39 line_end: usize,
40 column_start: usize,
41 column_end: usize,
42 is_primary: bool,
43 label: Option<String>,
44 suggested_replacement: Option<String>,
45 expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
46}
47
48#[derive(Deserialize)]
49struct FutureIncompatReport {
50 future_incompat_report: Vec<FutureBreakageItem>,
51}
52
53#[derive(Deserialize)]
54struct FutureBreakageItem {
55 diagnostic: Diagnostic,
56}
57
58impl DiagnosticSpan {
59 fn first_callsite_in_file(&self, file_name: &str) -> &DiagnosticSpan {
62 if self.file_name == file_name {
63 self
64 } else {
65 self.expansion
66 .as_ref()
67 .map(|origin| origin.span.first_callsite_in_file(file_name))
68 .unwrap_or(self)
69 }
70 }
71}
72
73#[derive(Deserialize, Clone)]
74struct DiagnosticSpanMacroExpansion {
75 span: DiagnosticSpan,
77
78 macro_decl_name: String,
80}
81
82#[derive(Deserialize, Clone)]
83struct DiagnosticCode {
84 code: String,
86}
87
88pub fn rustfix_diagnostics_only(output: &str) -> String {
89 output
90 .lines()
91 .filter(|line| line.starts_with('{') && serde_json::from_str::<Diagnostic>(line).is_ok())
92 .collect()
93}
94
95pub fn extract_rendered(output: &str) -> String {
96 output
97 .lines()
98 .filter_map(|line| {
99 if line.starts_with('{') {
100 if let Ok(diagnostic) = serde_json::from_str::<Diagnostic>(line) {
101 diagnostic.rendered
102 } else if let Ok(report) = serde_json::from_str::<FutureIncompatReport>(line) {
103 if report.future_incompat_report.is_empty() {
104 None
105 } else {
106 Some(format!(
107 "Future incompatibility report: {}",
108 report
109 .future_incompat_report
110 .into_iter()
111 .map(|item| {
112 format!(
113 "Future breakage diagnostic:\n{}",
114 item.diagnostic
115 .rendered
116 .unwrap_or_else(|| "Not rendered".to_string())
117 )
118 })
119 .collect::<String>()
120 ))
121 }
122 } else if serde_json::from_str::<ArtifactNotification>(line).is_ok() {
123 None
125 } else if serde_json::from_str::<UnusedExternNotification>(line).is_ok() {
126 None
128 } else {
129 Some(format!("{line}\n"))
133 }
134 } else {
135 Some(format!("{}\n", line))
137 }
138 })
139 .collect()
140}
141
142pub fn parse_output(file_name: &str, output: &str) -> Vec<Error> {
143 let mut errors = Vec::new();
144 for line in output.lines() {
145 match serde_json::from_str::<Diagnostic>(line) {
148 Ok(diagnostic) => push_actual_errors(&mut errors, &diagnostic, &[], file_name),
149 Err(_) => errors.push(Error {
150 line_num: None,
151 kind: ErrorKind::Raw,
152 msg: line.to_string(),
153 require_annotation: false,
154 }),
155 }
156 }
157 errors
158}
159
160fn push_actual_errors(
161 errors: &mut Vec<Error>,
162 diagnostic: &Diagnostic,
163 default_spans: &[&DiagnosticSpan],
164 file_name: &str,
165) {
166 let spans_info_in_this_file: Vec<_> = diagnostic
168 .spans
169 .iter()
170 .map(|span| (span.is_primary, span.first_callsite_in_file(file_name)))
171 .filter(|(_, span)| Path::new(&span.file_name) == Path::new(&file_name))
172 .collect();
173
174 let primary_spans: Vec<_> = spans_info_in_this_file
175 .iter()
176 .filter(|(is_primary, _)| *is_primary)
177 .map(|(_, span)| span)
178 .take(1) .cloned()
180 .collect();
181 let primary_spans = if primary_spans.is_empty() {
182 default_spans
185 } else {
186 &primary_spans
187 };
188
189 let with_code = |span: Option<&DiagnosticSpan>, text: &str| {
197 let span_str = match span {
206 Some(DiagnosticSpan { line_start, column_start, line_end, column_end, .. }) => {
207 format!("{line_start}:{column_start}: {line_end}:{column_end}")
208 }
209 None => format!("?:?: ?:?"),
210 };
211 match &diagnostic.code {
212 Some(code) => format!("{span_str}: {text} [{}]", code.code),
213 None => format!("{span_str}: {text}"),
214 }
215 };
216
217 let mut message_lines = diagnostic.message.lines();
220 let kind = ErrorKind::from_compiler_str(&diagnostic.level);
221 let first_line = message_lines.next().unwrap_or(&diagnostic.message);
222 if primary_spans.is_empty() {
223 static RE: OnceLock<Regex> = OnceLock::new();
224 let re_init =
225 || Regex::new(r"aborting due to \d+ previous errors?|\d+ warnings? emitted").unwrap();
226 errors.push(Error {
227 line_num: None,
228 kind,
229 msg: with_code(None, first_line),
230 require_annotation: diagnostic.level != "failure-note"
231 && !RE.get_or_init(re_init).is_match(first_line),
232 });
233 } else {
234 for span in primary_spans {
235 errors.push(Error {
236 line_num: Some(span.line_start),
237 kind,
238 msg: with_code(Some(span), first_line),
239 require_annotation: true,
240 });
241 }
242 }
243 for next_line in message_lines {
244 if primary_spans.is_empty() {
245 errors.push(Error {
246 line_num: None,
247 kind,
248 msg: with_code(None, next_line),
249 require_annotation: false,
250 });
251 } else {
252 for span in primary_spans {
253 errors.push(Error {
254 line_num: Some(span.line_start),
255 kind,
256 msg: with_code(Some(span), next_line),
257 require_annotation: false,
258 });
259 }
260 }
261 }
262
263 for span in primary_spans {
265 if let Some(ref suggested_replacement) = span.suggested_replacement {
266 for (index, line) in suggested_replacement.lines().enumerate() {
267 errors.push(Error {
268 line_num: Some(span.line_start + index),
269 kind: ErrorKind::Suggestion,
270 msg: line.to_string(),
271 require_annotation: !line.is_empty(),
274 });
275 }
276 }
277 }
278
279 for span in primary_spans {
281 if let Some(frame) = &span.expansion {
282 push_backtrace(errors, frame, file_name);
283 }
284 }
285
286 for (_, span) in spans_info_in_this_file {
288 if let Some(label) = &span.label {
289 errors.push(Error {
290 line_num: Some(span.line_start),
291 kind: ErrorKind::Note,
292 msg: label.clone(),
293 require_annotation: !label.is_empty(),
295 });
296 }
297 }
298
299 for child in &diagnostic.children {
301 push_actual_errors(errors, child, primary_spans, file_name);
302 }
303}
304
305fn push_backtrace(
306 errors: &mut Vec<Error>,
307 expansion: &DiagnosticSpanMacroExpansion,
308 file_name: &str,
309) {
310 if Path::new(&expansion.span.file_name) == Path::new(&file_name) {
311 errors.push(Error {
312 line_num: Some(expansion.span.line_start),
313 kind: ErrorKind::Note,
314 msg: format!("in this expansion of {}", expansion.macro_decl_name),
315 require_annotation: true,
316 });
317 }
318
319 if let Some(previous_expansion) = &expansion.span.expansion {
320 push_backtrace(errors, previous_expansion, file_name);
321 }
322}