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::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    /// Returns the deepest source span in the macro call stack with a given file name.
62    /// This is either the supplied span, or the span for some macro callsite that expanded to it.
63    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 where macro was applied to generate this code
78    span: DiagnosticSpan,
79
80    /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]")
81    macro_decl_name: String,
82}
83
84#[derive(Deserialize, Clone)]
85struct DiagnosticCode {
86    /// The code itself.
87    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                    // Ignore the notification.
126                    None
127                } else if serde_json::from_str::<UnusedExternNotification>(line).is_ok() {
128                    // Ignore the notification.
129                    None
130                } else {
131                    // This function is called for both compiler and non-compiler output,
132                    // so if the line isn't recognized as JSON from the compiler then
133                    // just print it as-is.
134                    Some(format!("{line}\n"))
135                }
136            } else {
137                // preserve non-JSON lines, such as ICEs
138                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    // The compiler sometimes intermingles non-JSON stuff into the
150    // output.  This hack just skips over such lines. Yuck.
151    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                // Ignore the future compat report message - this is handled
160                // by `extract_rendered`
161                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    // In case of macro expansions, we need to get the span of the callsite
187    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) // sometimes we have more than one showing up in the json; pick first
201        .cloned()
202        .collect();
203    let primary_spans = if primary_spans.is_empty() {
204        // subdiagnostics often don't have a span of their own;
205        // inherit the span from the parent in that case
206        default_spans
207    } else {
208        &primary_spans
209    };
210
211    // We break the output into multiple lines, and then append the
212    // [E123] to every line in the output. This may be overkill.  The
213    // intention was to match existing tests that do things like "//|
214    // found `i32` [E123]" and expect to match that somewhere, and yet
215    // also ensure that `//~ ERROR E123` *always* works. The
216    // assumption is that these multi-line error messages are on their
217    // way out anyhow.
218    let with_code = |span: Option<&DiagnosticSpan>, text: &str| {
219        // FIXME(#33000) -- it'd be better to use a dedicated
220        // UI harness than to include the line/col number like
221        // this, but some current tests rely on it.
222        //
223        // Note: Do NOT include the filename. These can easily
224        // cause false matches where the expected message
225        // appears in the filename, and hence the message
226        // changes but the test still passes.
227        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    // Convert multi-line messages into multiple expected
240    // errors. We expect to replace these with something
241    // more structured shortly anyhow.
242    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    // If the message has a suggestion, register that.
283    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    // Add notes for the backtrace
296    for span in primary_spans {
297        if let Some(frame) = &span.expansion {
298            push_backtrace(expected_errors, frame, file_name);
299        }
300    }
301
302    // Add notes for any labels that appear in the message.
303    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    // Flatten out the children.
312    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}