compiletest/
json.rs

1//! These structs are a subset of the ones found in `rustc_errors::json`.
2
3use 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    /// Returns the deepest source span in the macro call stack with a given file name.
58    /// This is either the supplied span, or the span for some macro callsite that expanded to it.
59    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 where macro was applied to generate this code
74    span: DiagnosticSpan,
75
76    /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]")
77    macro_decl_name: String,
78}
79
80#[derive(Deserialize, Clone)]
81struct DiagnosticCode {
82    /// The code itself.
83    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                    // Ignore the notification.
122                    None
123                } else if serde_json::from_str::<UnusedExternNotification>(line).is_ok() {
124                    // Ignore the notification.
125                    None
126                } else {
127                    // This function is called for both compiler and non-compiler output,
128                    // so if the line isn't recognized as JSON from the compiler then
129                    // just print it as-is.
130                    Some(format!("{line}\n"))
131                }
132            } else {
133                // preserve non-JSON lines, such as ICEs
134                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        // Compiler can emit non-json lines in non-`--error-format=json` modes,
144        // and in some situations even in json mode.
145        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    // In case of macro expansions, we need to get the span of the callsite
166    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) // sometimes we have more than one showing up in the json; pick first
178        .cloned()
179        .collect();
180    let primary_spans = if primary_spans.is_empty() {
181        // subdiagnostics often don't have a span of their own;
182        // inherit the span from the parent in that case
183        default_spans
184    } else {
185        &primary_spans
186    };
187
188    // We break the output into multiple lines, and then append the
189    // [E123] to every line in the output. This may be overkill.  The
190    // intention was to match existing tests that do things like "//|
191    // found `i32` [E123]" and expect to match that somewhere, and yet
192    // also ensure that `//~ ERROR E123` *always* works. The
193    // assumption is that these multi-line error messages are on their
194    // way out anyhow.
195    let with_code = |text| match &diagnostic.code {
196        Some(code) => format!("{text} [{}]", code.code),
197        None => format!("{text}"),
198    };
199
200    // Convert multi-line messages into multiple errors.
201    // We expect to replace these with something more structured anyhow.
202    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    // If the message has a suggestion, register that.
251    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                    // Empty suggestions (suggestions to remove something) are common
260                    // and annotating them in source is not useful.
261                    require_annotation: !line.is_empty(),
262                });
263            }
264        }
265    }
266
267    // Add notes for the backtrace
268    for span in primary_spans {
269        if let Some(frame) = &span.expansion {
270            push_backtrace(errors, frame, file_name);
271        }
272    }
273
274    // Add notes for any labels that appear in the message.
275    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                // Empty labels (only underlining spans) are common and do not need annotations.
283                require_annotation: !label.is_empty(),
284            });
285        }
286    }
287
288    // Flatten out the children.
289    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}