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 column_start: usize,
40 is_primary: bool,
41 label: Option<String>,
42 suggested_replacement: Option<String>,
43 expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
44}
45
46#[derive(Deserialize)]
47struct FutureIncompatReport {
48 future_incompat_report: Vec<FutureBreakageItem>,
49}
50
51#[derive(Deserialize)]
52struct FutureBreakageItem {
53 diagnostic: Diagnostic,
54}
55
56impl DiagnosticSpan {
57 fn first_callsite_in_file(&self, file_name: &str) -> &DiagnosticSpan {
60 if self.file_name == file_name {
61 self
62 } else {
63 self.expansion
64 .as_ref()
65 .map(|origin| origin.span.first_callsite_in_file(file_name))
66 .unwrap_or(self)
67 }
68 }
69}
70
71#[derive(Deserialize, Clone)]
72struct DiagnosticSpanMacroExpansion {
73 span: DiagnosticSpan,
75
76 macro_decl_name: String,
78}
79
80#[derive(Deserialize, Clone)]
81struct DiagnosticCode {
82 code: String,
84}
85
86pub fn rustfix_diagnostics_only(output: &str) -> String {
87 output
88 .lines()
89 .filter(|line| line.starts_with('{') && serde_json::from_str::<Diagnostic>(line).is_ok())
90 .collect()
91}
92
93pub fn extract_rendered(output: &str) -> String {
94 output
95 .lines()
96 .filter_map(|line| {
97 if line.starts_with('{') {
98 if let Ok(diagnostic) = serde_json::from_str::<Diagnostic>(line) {
99 diagnostic.rendered
100 } else if let Ok(report) = serde_json::from_str::<FutureIncompatReport>(line) {
101 if report.future_incompat_report.is_empty() {
102 None
103 } else {
104 Some(format!(
105 "Future incompatibility report: {}",
106 report
107 .future_incompat_report
108 .into_iter()
109 .map(|item| {
110 format!(
111 "Future breakage diagnostic:\n{}",
112 item.diagnostic
113 .rendered
114 .unwrap_or_else(|| "Not rendered".to_string())
115 )
116 })
117 .collect::<String>()
118 ))
119 }
120 } else if serde_json::from_str::<ArtifactNotification>(line).is_ok() {
121 None
123 } else if serde_json::from_str::<UnusedExternNotification>(line).is_ok() {
124 None
126 } else {
127 Some(format!("{line}\n"))
131 }
132 } else {
133 Some(format!("{}\n", line))
135 }
136 })
137 .collect()
138}
139
140pub fn parse_output(file_name: &str, output: &str) -> Vec<Error> {
141 let mut errors = Vec::new();
142 for line in output.lines() {
143 match serde_json::from_str::<Diagnostic>(line) {
146 Ok(diagnostic) => push_actual_errors(&mut errors, &diagnostic, &[], file_name),
147 Err(_) => errors.push(Error {
148 line_num: None,
149 column_num: None,
150 kind: ErrorKind::Raw,
151 msg: line.to_string(),
152 require_annotation: false,
153 }),
154 }
155 }
156 errors
157}
158
159fn push_actual_errors(
160 errors: &mut Vec<Error>,
161 diagnostic: &Diagnostic,
162 default_spans: &[&DiagnosticSpan],
163 file_name: &str,
164) {
165 let spans_info_in_this_file: Vec<_> = diagnostic
167 .spans
168 .iter()
169 .map(|span| (span.is_primary, span.first_callsite_in_file(file_name)))
170 .filter(|(_, span)| Path::new(&span.file_name) == Path::new(&file_name))
171 .collect();
172
173 let primary_spans: Vec<_> = spans_info_in_this_file
174 .iter()
175 .filter(|(is_primary, _)| *is_primary)
176 .map(|(_, span)| span)
177 .take(1) .cloned()
179 .collect();
180 let primary_spans = if primary_spans.is_empty() {
181 default_spans
184 } else {
185 &primary_spans
186 };
187
188 let with_code = |text| match &diagnostic.code {
196 Some(code) => format!("{text} [{}]", code.code),
197 None => format!("{text}"),
198 };
199
200 let mut message_lines = diagnostic.message.lines();
203 let kind = ErrorKind::from_compiler_str(&diagnostic.level);
204 let first_line = message_lines.next().unwrap_or(&diagnostic.message);
205 if primary_spans.is_empty() {
206 static RE: OnceLock<Regex> = OnceLock::new();
207 let re_init =
208 || Regex::new(r"aborting due to \d+ previous errors?|\d+ warnings? emitted").unwrap();
209 errors.push(Error {
210 line_num: None,
211 column_num: None,
212 kind,
213 msg: with_code(first_line),
214 require_annotation: diagnostic.level != "failure-note"
215 && !RE.get_or_init(re_init).is_match(first_line),
216 });
217 } else {
218 for span in primary_spans {
219 errors.push(Error {
220 line_num: Some(span.line_start),
221 column_num: Some(span.column_start),
222 kind,
223 msg: with_code(first_line),
224 require_annotation: true,
225 });
226 }
227 }
228 for next_line in message_lines {
229 if primary_spans.is_empty() {
230 errors.push(Error {
231 line_num: None,
232 column_num: None,
233 kind,
234 msg: with_code(next_line),
235 require_annotation: false,
236 });
237 } else {
238 for span in primary_spans {
239 errors.push(Error {
240 line_num: Some(span.line_start),
241 column_num: Some(span.column_start),
242 kind,
243 msg: with_code(next_line),
244 require_annotation: false,
245 });
246 }
247 }
248 }
249
250 for span in primary_spans {
252 if let Some(ref suggested_replacement) = span.suggested_replacement {
253 for (index, line) in suggested_replacement.lines().enumerate() {
254 errors.push(Error {
255 line_num: Some(span.line_start + index),
256 column_num: Some(span.column_start),
257 kind: ErrorKind::Suggestion,
258 msg: line.to_string(),
259 require_annotation: !line.is_empty(),
262 });
263 }
264 }
265 }
266
267 for span in primary_spans {
269 if let Some(frame) = &span.expansion {
270 push_backtrace(errors, frame, file_name);
271 }
272 }
273
274 for (_, span) in spans_info_in_this_file {
276 if let Some(label) = &span.label {
277 errors.push(Error {
278 line_num: Some(span.line_start),
279 column_num: Some(span.column_start),
280 kind: ErrorKind::Note,
281 msg: label.clone(),
282 require_annotation: !label.is_empty(),
284 });
285 }
286 }
287
288 for child in &diagnostic.children {
290 push_actual_errors(errors, child, primary_spans, file_name);
291 }
292}
293
294fn push_backtrace(
295 errors: &mut Vec<Error>,
296 expansion: &DiagnosticSpanMacroExpansion,
297 file_name: &str,
298) {
299 if Path::new(&expansion.span.file_name) == Path::new(&file_name) {
300 errors.push(Error {
301 line_num: Some(expansion.span.line_start),
302 column_num: Some(expansion.span.column_start),
303 kind: ErrorKind::Note,
304 msg: format!("in this expansion of {}", expansion.macro_decl_name),
305 require_annotation: true,
306 });
307 }
308
309 if let Some(previous_expansion) = &expansion.span.expansion {
310 push_backtrace(errors, previous_expansion, file_name);
311 }
312}