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    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) -> Vec<Error> {
143    let mut errors = Vec::new();
144    for line in output.lines() {
145        // Compiler can emit non-json lines in non-`--error-format=json` modes,
146        // and in some situations even in json mode.
147        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    // In case of macro expansions, we need to get the span of the callsite
167    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) // sometimes we have more than one showing up in the json; pick first
179        .cloned()
180        .collect();
181    let primary_spans = if primary_spans.is_empty() {
182        // subdiagnostics often don't have a span of their own;
183        // inherit the span from the parent in that case
184        default_spans
185    } else {
186        &primary_spans
187    };
188
189    // We break the output into multiple lines, and then append the
190    // [E123] to every line in the output. This may be overkill.  The
191    // intention was to match existing tests that do things like "//|
192    // found `i32` [E123]" and expect to match that somewhere, and yet
193    // also ensure that `//~ ERROR E123` *always* works. The
194    // assumption is that these multi-line error messages are on their
195    // way out anyhow.
196    let with_code = |span: Option<&DiagnosticSpan>, text: &str| {
197        // FIXME(#33000) -- it'd be better to use a dedicated
198        // UI harness than to include the line/col number like
199        // this, but some current tests rely on it.
200        //
201        // Note: Do NOT include the filename. These can easily
202        // cause false matches where the expected message
203        // appears in the filename, and hence the message
204        // changes but the test still passes.
205        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    // Convert multi-line messages into multiple errors.
218    // We expect to replace these with something more structured anyhow.
219    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    // If the message has a suggestion, register that.
264    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                    // Empty suggestions (suggestions to remove something) are common
272                    // and annotating them in source is not useful.
273                    require_annotation: !line.is_empty(),
274                });
275            }
276        }
277    }
278
279    // Add notes for the backtrace
280    for span in primary_spans {
281        if let Some(frame) = &span.expansion {
282            push_backtrace(errors, frame, file_name);
283        }
284    }
285
286    // Add notes for any labels that appear in the message.
287    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                // Empty labels (only underlining spans) are common and do not need annotations.
294                require_annotation: !label.is_empty(),
295            });
296        }
297    }
298
299    // Flatten out the children.
300    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}