]> git.lizzy.rs Git - rust.git/blob - src/tools/compiletest/src/json.rs
Auto merge of #57770 - Zoxc:no-hash-query, r=michaelwoerister
[rust.git] / src / tools / compiletest / src / json.rs
1 use crate::errors::{Error, ErrorKind};
2 use crate::runtest::ProcRes;
3 use serde_json;
4 use std::path::Path;
5 use std::str::FromStr;
6
7 // These structs are a subset of the ones found in
8 // `syntax::json`.
9
10 #[derive(Deserialize)]
11 struct Diagnostic {
12     message: String,
13     code: Option<DiagnosticCode>,
14     level: String,
15     spans: Vec<DiagnosticSpan>,
16     children: Vec<Diagnostic>,
17     rendered: Option<String>,
18 }
19
20 #[derive(Deserialize, Clone)]
21 struct DiagnosticSpan {
22     file_name: String,
23     line_start: usize,
24     line_end: usize,
25     column_start: usize,
26     column_end: usize,
27     is_primary: bool,
28     label: Option<String>,
29     suggested_replacement: Option<String>,
30     expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
31 }
32
33 impl DiagnosticSpan {
34     /// Returns the deepest source span in the macro call stack with a given file name.
35     /// This is either the supplied span, or the span for some macro callsite that expanded to it.
36     fn first_callsite_in_file(&self, file_name: &str) -> &DiagnosticSpan {
37         if self.file_name == file_name {
38             self
39         } else {
40             self.expansion
41                 .as_ref()
42                 .map(|origin| origin.span.first_callsite_in_file(file_name))
43                 .unwrap_or(self)
44         }
45     }
46 }
47
48 #[derive(Deserialize, Clone)]
49 struct DiagnosticSpanMacroExpansion {
50     /// span where macro was applied to generate this code
51     span: DiagnosticSpan,
52
53     /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]")
54     macro_decl_name: String,
55 }
56
57 #[derive(Deserialize, Clone)]
58 struct DiagnosticCode {
59     /// The code itself.
60     code: String,
61     /// An explanation for the code.
62     explanation: Option<String>,
63 }
64
65 pub fn extract_rendered(output: &str, proc_res: &ProcRes) -> String {
66     output
67         .lines()
68         .filter_map(|line| {
69             if line.starts_with('{') {
70                 match serde_json::from_str::<Diagnostic>(line) {
71                     Ok(diagnostic) => diagnostic.rendered,
72                     Err(error) => {
73                         proc_res.fatal(Some(&format!(
74                             "failed to decode compiler output as json: \
75                              `{}`\nline: {}\noutput: {}",
76                             error, line, output
77                         )));
78                     }
79                 }
80             } else {
81                 None
82             }
83         })
84         .collect()
85 }
86
87 pub fn parse_output(file_name: &str, output: &str, proc_res: &ProcRes) -> Vec<Error> {
88     output
89         .lines()
90         .flat_map(|line| parse_line(file_name, line, output, proc_res))
91         .collect()
92 }
93
94 fn parse_line(file_name: &str, line: &str, output: &str, proc_res: &ProcRes) -> Vec<Error> {
95     // The compiler sometimes intermingles non-JSON stuff into the
96     // output.  This hack just skips over such lines. Yuck.
97     if line.starts_with('{') {
98         match serde_json::from_str::<Diagnostic>(line) {
99             Ok(diagnostic) => {
100                 let mut expected_errors = vec![];
101                 push_expected_errors(&mut expected_errors, &diagnostic, &[], file_name);
102                 expected_errors
103             }
104             Err(error) => {
105                 proc_res.fatal(Some(&format!(
106                     "failed to decode compiler output as json: \
107                      `{}`\nline: {}\noutput: {}",
108                     error, line, output
109                 )));
110             }
111         }
112     } else {
113         vec![]
114     }
115 }
116
117 fn push_expected_errors(
118     expected_errors: &mut Vec<Error>,
119     diagnostic: &Diagnostic,
120     default_spans: &[&DiagnosticSpan],
121     file_name: &str,
122 ) {
123     // In case of macro expansions, we need to get the span of the callsite
124     let spans_info_in_this_file: Vec<_> = diagnostic
125         .spans
126         .iter()
127         .map(|span| (span.is_primary, span.first_callsite_in_file(file_name)))
128         .filter(|(_, span)| Path::new(&span.file_name) == Path::new(&file_name))
129         .collect();
130
131     let spans_in_this_file: Vec<_> = spans_info_in_this_file.iter()
132         .map(|(_, span)| span)
133         .collect();
134
135     let primary_spans: Vec<_> = spans_info_in_this_file.iter()
136         .filter(|(is_primary, _)| *is_primary)
137         .map(|(_, span)| span)
138         .take(1) // sometimes we have more than one showing up in the json; pick first
139         .cloned()
140         .collect();
141     let primary_spans = if primary_spans.is_empty() {
142         // subdiagnostics often don't have a span of their own;
143         // inherit the span from the parent in that case
144         default_spans
145     } else {
146         &primary_spans
147     };
148
149     // We break the output into multiple lines, and then append the
150     // [E123] to every line in the output. This may be overkill.  The
151     // intention was to match existing tests that do things like "//|
152     // found `i32` [E123]" and expect to match that somewhere, and yet
153     // also ensure that `//~ ERROR E123` *always* works. The
154     // assumption is that these multi-line error messages are on their
155     // way out anyhow.
156     let with_code = |span: &DiagnosticSpan, text: &str| {
157         match diagnostic.code {
158             Some(ref code) =>
159                 // FIXME(#33000) -- it'd be better to use a dedicated
160                 // UI harness than to include the line/col number like
161                 // this, but some current tests rely on it.
162                 //
163                 // Note: Do NOT include the filename. These can easily
164                 // cause false matches where the expected message
165                 // appears in the filename, and hence the message
166                 // changes but the test still passes.
167                 format!("{}:{}: {}:{}: {} [{}]",
168                         span.line_start, span.column_start,
169                         span.line_end, span.column_end,
170                         text, code.code.clone()),
171             None =>
172                 // FIXME(#33000) -- it'd be better to use a dedicated UI harness
173                 format!("{}:{}: {}:{}: {}",
174                         span.line_start, span.column_start,
175                         span.line_end, span.column_end,
176                         text),
177         }
178     };
179
180     // Convert multi-line messages into multiple expected
181     // errors. We expect to replace these with something
182     // more structured shortly anyhow.
183     let mut message_lines = diagnostic.message.lines();
184     if let Some(first_line) = message_lines.next() {
185         for span in primary_spans {
186             let msg = with_code(span, first_line);
187             let kind = ErrorKind::from_str(&diagnostic.level).ok();
188             expected_errors.push(Error {
189                 line_num: span.line_start,
190                 kind,
191                 msg,
192             });
193         }
194     }
195     for next_line in message_lines {
196         for span in primary_spans {
197             expected_errors.push(Error {
198                 line_num: span.line_start,
199                 kind: None,
200                 msg: with_code(span, next_line),
201             });
202         }
203     }
204
205     // If the message has a suggestion, register that.
206     for span in primary_spans {
207         if let Some(ref suggested_replacement) = span.suggested_replacement {
208             for (index, line) in suggested_replacement.lines().enumerate() {
209                 expected_errors.push(Error {
210                     line_num: span.line_start + index,
211                     kind: Some(ErrorKind::Suggestion),
212                     msg: line.to_string(),
213                 });
214             }
215         }
216     }
217
218     // Add notes for the backtrace
219     for span in primary_spans {
220         for frame in &span.expansion {
221             push_backtrace(expected_errors, frame, file_name);
222         }
223     }
224
225     // Add notes for any labels that appear in the message.
226     for span in spans_in_this_file
227         .iter()
228         .filter(|span| span.label.is_some())
229     {
230         expected_errors.push(Error {
231             line_num: span.line_start,
232             kind: Some(ErrorKind::Note),
233             msg: span.label.clone().unwrap(),
234         });
235     }
236
237     // Flatten out the children.
238     for child in &diagnostic.children {
239         push_expected_errors(expected_errors, child, primary_spans, file_name);
240     }
241 }
242
243 fn push_backtrace(
244     expected_errors: &mut Vec<Error>,
245     expansion: &DiagnosticSpanMacroExpansion,
246     file_name: &str,
247 ) {
248     if Path::new(&expansion.span.file_name) == Path::new(&file_name) {
249         expected_errors.push(Error {
250             line_num: expansion.span.line_start,
251             kind: Some(ErrorKind::Note),
252             msg: format!("in this expansion of {}", expansion.macro_decl_name),
253         });
254     }
255
256     for previous_expansion in &expansion.span.expansion {
257         push_backtrace(expected_errors, previous_expansion, file_name);
258     }
259 }