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;
5
6use serde::Deserialize;
7
8use crate::errors::{Error, ErrorKind};
9use crate::runtest::ProcRes;
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    /// Returns the deepest source span in the macro call stack with a given file name.
60    /// This is either the supplied span, or the span for some macro callsite that expanded to it.
61    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 where macro was applied to generate this code
76    span: DiagnosticSpan,
77
78    /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]")
79    macro_decl_name: String,
80}
81
82#[derive(Deserialize, Clone)]
83struct DiagnosticCode {
84    /// The code itself.
85    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                    // Ignore the notification.
124                    None
125                } else if serde_json::from_str::<UnusedExternNotification>(line).is_ok() {
126                    // Ignore the notification.
127                    None
128                } else {
129                    // This function is called for both compiler and non-compiler output,
130                    // so if the line isn't recognized as JSON from the compiler then
131                    // just print it as-is.
132                    Some(format!("{line}\n"))
133                }
134            } else {
135                // preserve non-JSON lines, such as ICEs
136                Some(format!("{}\n", line))
137            }
138        })
139        .collect()
140}
141
142pub fn parse_output(file_name: &str, output: &str, proc_res: &ProcRes) -> Vec<Error> {
143    output.lines().flat_map(|line| parse_line(file_name, line, output, proc_res)).collect()
144}
145
146fn parse_line(file_name: &str, line: &str, output: &str, proc_res: &ProcRes) -> Vec<Error> {
147    // The compiler sometimes intermingles non-JSON stuff into the
148    // output.  This hack just skips over such lines. Yuck.
149    if line.starts_with('{') {
150        match serde_json::from_str::<Diagnostic>(line) {
151            Ok(diagnostic) => {
152                let mut expected_errors = vec![];
153                push_expected_errors(&mut expected_errors, &diagnostic, &[], file_name);
154                expected_errors
155            }
156            Err(error) => {
157                // Ignore the future compat report message - this is handled
158                // by `extract_rendered`
159                if serde_json::from_str::<FutureIncompatReport>(line).is_ok() {
160                    vec![]
161                } else {
162                    proc_res.fatal(
163                        Some(&format!(
164                            "failed to decode compiler output as json: \
165                         `{}`\nline: {}\noutput: {}",
166                            error, line, output
167                        )),
168                        || (),
169                    );
170                }
171            }
172        }
173    } else {
174        vec![]
175    }
176}
177
178fn push_expected_errors(
179    expected_errors: &mut Vec<Error>,
180    diagnostic: &Diagnostic,
181    default_spans: &[&DiagnosticSpan],
182    file_name: &str,
183) {
184    // In case of macro expansions, we need to get the span of the callsite
185    let spans_info_in_this_file: Vec<_> = diagnostic
186        .spans
187        .iter()
188        .map(|span| (span.is_primary, span.first_callsite_in_file(file_name)))
189        .filter(|(_, span)| Path::new(&span.file_name) == Path::new(&file_name))
190        .collect();
191
192    let spans_in_this_file: Vec<_> = spans_info_in_this_file.iter().map(|(_, span)| span).collect();
193
194    let primary_spans: Vec<_> = spans_info_in_this_file
195        .iter()
196        .filter(|(is_primary, _)| *is_primary)
197        .map(|(_, span)| span)
198        .take(1) // sometimes we have more than one showing up in the json; pick first
199        .cloned()
200        .collect();
201    let primary_spans = if primary_spans.is_empty() {
202        // subdiagnostics often don't have a span of their own;
203        // inherit the span from the parent in that case
204        default_spans
205    } else {
206        &primary_spans
207    };
208
209    // We break the output into multiple lines, and then append the
210    // [E123] to every line in the output. This may be overkill.  The
211    // intention was to match existing tests that do things like "//|
212    // found `i32` [E123]" and expect to match that somewhere, and yet
213    // also ensure that `//~ ERROR E123` *always* works. The
214    // assumption is that these multi-line error messages are on their
215    // way out anyhow.
216    let with_code = |span: &DiagnosticSpan, text: &str| {
217        match diagnostic.code {
218            Some(ref code) =>
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            {
228                format!(
229                    "{}:{}: {}:{}: {} [{}]",
230                    span.line_start,
231                    span.column_start,
232                    span.line_end,
233                    span.column_end,
234                    text,
235                    code.code.clone()
236                )
237            }
238            None =>
239            // FIXME(#33000) -- it'd be better to use a dedicated UI harness
240            {
241                format!(
242                    "{}:{}: {}:{}: {}",
243                    span.line_start, span.column_start, span.line_end, span.column_end, text
244                )
245            }
246        }
247    };
248
249    // Convert multi-line messages into multiple expected
250    // errors. We expect to replace these with something
251    // more structured shortly anyhow.
252    let mut message_lines = diagnostic.message.lines();
253    if let Some(first_line) = message_lines.next() {
254        for span in primary_spans {
255            let msg = with_code(span, first_line);
256            let kind = ErrorKind::from_str(&diagnostic.level).ok();
257            expected_errors.push(Error { line_num: span.line_start, kind, msg });
258        }
259    }
260    for next_line in message_lines {
261        for span in primary_spans {
262            expected_errors.push(Error {
263                line_num: span.line_start,
264                kind: None,
265                msg: with_code(span, next_line),
266            });
267        }
268    }
269
270    // If the message has a suggestion, register that.
271    for span in primary_spans {
272        if let Some(ref suggested_replacement) = span.suggested_replacement {
273            for (index, line) in suggested_replacement.lines().enumerate() {
274                expected_errors.push(Error {
275                    line_num: span.line_start + index,
276                    kind: Some(ErrorKind::Suggestion),
277                    msg: line.to_string(),
278                });
279            }
280        }
281    }
282
283    // Add notes for the backtrace
284    for span in primary_spans {
285        if let Some(frame) = &span.expansion {
286            push_backtrace(expected_errors, frame, file_name);
287        }
288    }
289
290    // Add notes for any labels that appear in the message.
291    for span in spans_in_this_file.iter().filter(|span| span.label.is_some()) {
292        expected_errors.push(Error {
293            line_num: span.line_start,
294            kind: Some(ErrorKind::Note),
295            msg: span.label.clone().unwrap(),
296        });
297    }
298
299    // Flatten out the children.
300    for child in &diagnostic.children {
301        push_expected_errors(expected_errors, child, primary_spans, file_name);
302    }
303}
304
305fn push_backtrace(
306    expected_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        expected_errors.push(Error {
312            line_num: expansion.span.line_start,
313            kind: Some(ErrorKind::Note),
314            msg: format!("in this expansion of {}", expansion.macro_decl_name),
315        });
316    }
317
318    if let Some(previous_expansion) = &expansion.span.expansion {
319        push_backtrace(expected_errors, previous_expansion, file_name);
320    }
321}