]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/error_codes_check.rs
Add 'compiler/rustc_smir/' from commit '9abcb5c7b574cf316eb23d3f469187bb86ba3019'
[rust.git] / src / tools / tidy / src / error_codes_check.rs
1 //! Checks that all error codes have at least one test to prevent having error
2 //! codes that are silently not thrown by the compiler anymore.
3
4 use std::collections::{HashMap, HashSet};
5 use std::ffi::OsStr;
6 use std::fs::read_to_string;
7 use std::path::Path;
8
9 use regex::Regex;
10
11 // A few of those error codes can't be tested but all the others can and *should* be tested!
12 const EXEMPTED_FROM_TEST: &[&str] = &[
13     "E0279", "E0313", "E0377", "E0461", "E0462", "E0465", "E0476", "E0514", "E0519", "E0523",
14     "E0554", "E0640", "E0717", "E0729",
15 ];
16
17 // Some error codes don't have any tests apparently...
18 const IGNORE_EXPLANATION_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0729"];
19
20 // If the file path contains any of these, we don't want to try to extract error codes from it.
21 //
22 // We need to declare each path in the windows version (with backslash).
23 const PATHS_TO_IGNORE_FOR_EXTRACTION: &[&str] =
24     &["src/test/", "src\\test\\", "src/doc/", "src\\doc\\", "src/tools/", "src\\tools\\"];
25
26 #[derive(Default, Debug)]
27 struct ErrorCodeStatus {
28     has_test: bool,
29     has_explanation: bool,
30     is_used: bool,
31 }
32
33 fn check_error_code_explanation(
34     f: &str,
35     error_codes: &mut HashMap<String, ErrorCodeStatus>,
36     err_code: String,
37 ) -> bool {
38     let mut invalid_compile_fail_format = false;
39     let mut found_error_code = false;
40
41     for line in f.lines() {
42         let s = line.trim();
43         if s.starts_with("```") {
44             if s.contains("compile_fail") && s.contains('E') {
45                 if !found_error_code {
46                     error_codes.get_mut(&err_code).map(|x| x.has_test = true);
47                     found_error_code = true;
48                 }
49             } else if s.contains("compile-fail") {
50                 invalid_compile_fail_format = true;
51             }
52         } else if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
53             if !found_error_code {
54                 error_codes.get_mut(&err_code).map(|x| x.has_test = true);
55                 found_error_code = true;
56             }
57         }
58     }
59     invalid_compile_fail_format
60 }
61
62 fn check_if_error_code_is_test_in_explanation(f: &str, err_code: &str) -> bool {
63     let mut ignore_found = false;
64
65     for line in f.lines() {
66         let s = line.trim();
67         if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
68             return true;
69         }
70         if s.starts_with("```") {
71             if s.contains("compile_fail") && s.contains(err_code) {
72                 return true;
73             } else if s.contains("ignore") {
74                 // It's very likely that we can't actually make it fail compilation...
75                 ignore_found = true;
76             }
77         }
78     }
79     ignore_found
80 }
81
82 macro_rules! some_or_continue {
83     ($e:expr) => {
84         match $e {
85             Some(e) => e,
86             None => continue,
87         }
88     };
89 }
90
91 fn extract_error_codes(
92     f: &str,
93     error_codes: &mut HashMap<String, ErrorCodeStatus>,
94     path: &Path,
95     errors: &mut Vec<String>,
96 ) {
97     let mut reached_no_explanation = false;
98
99     for line in f.lines() {
100         let s = line.trim();
101         if !reached_no_explanation && s.starts_with('E') && s.contains("include_str!(\"") {
102             let err_code = s
103                 .split_once(':')
104                 .expect(
105                     format!(
106                         "Expected a line with the format `E0xxx: include_str!(\"..\")`, but got {} \
107                          without a `:` delimiter",
108                         s,
109                     )
110                     .as_str(),
111                 )
112                 .0
113                 .to_owned();
114             error_codes.entry(err_code.clone()).or_default().has_explanation = true;
115
116             // Now we extract the tests from the markdown file!
117             let md_file_name = match s.split_once("include_str!(\"") {
118                 None => continue,
119                 Some((_, md)) => match md.split_once("\")") {
120                     None => continue,
121                     Some((file_name, _)) => file_name,
122                 },
123             };
124             let path = some_or_continue!(path.parent())
125                 .join(md_file_name)
126                 .canonicalize()
127                 .expect("failed to canonicalize error explanation file path");
128             match read_to_string(&path) {
129                 Ok(content) => {
130                     let has_test = check_if_error_code_is_test_in_explanation(&content, &err_code);
131                     if !has_test && !IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
132                         errors.push(format!(
133                             "`{}` doesn't use its own error code in compile_fail example",
134                             path.display(),
135                         ));
136                     } else if has_test && IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
137                         errors.push(format!(
138                             "`{}` has a compile_fail example with its own error code, it shouldn't \
139                              be listed in IGNORE_EXPLANATION_CHECK!",
140                             path.display(),
141                         ));
142                     }
143                     if check_error_code_explanation(&content, error_codes, err_code) {
144                         errors.push(format!(
145                             "`{}` uses invalid tag `compile-fail` instead of `compile_fail`",
146                             path.display(),
147                         ));
148                     }
149                 }
150                 Err(e) => {
151                     eprintln!("Couldn't read `{}`: {}", path.display(), e);
152                 }
153             }
154         } else if reached_no_explanation && s.starts_with('E') {
155             let err_code = match s.split_once(',') {
156                 None => s,
157                 Some((err_code, _)) => err_code,
158             }
159             .to_string();
160             if !error_codes.contains_key(&err_code) {
161                 // this check should *never* fail!
162                 error_codes.insert(err_code, ErrorCodeStatus::default());
163             }
164         } else if s == ";" {
165             reached_no_explanation = true;
166         }
167     }
168 }
169
170 fn extract_error_codes_from_tests(f: &str, error_codes: &mut HashMap<String, ErrorCodeStatus>) {
171     for line in f.lines() {
172         let s = line.trim();
173         if s.starts_with("error[E") || s.starts_with("warning[E") {
174             let err_code = match s.split_once(']') {
175                 None => continue,
176                 Some((err_code, _)) => match err_code.split_once('[') {
177                     None => continue,
178                     Some((_, err_code)) => err_code,
179                 },
180             };
181             error_codes.entry(err_code.to_owned()).or_default().has_test = true;
182         }
183     }
184 }
185
186 fn extract_error_codes_from_source(
187     f: &str,
188     error_codes: &mut HashMap<String, ErrorCodeStatus>,
189     regex: &Regex,
190 ) {
191     for line in f.lines() {
192         if line.trim_start().starts_with("//") {
193             continue;
194         }
195         for cap in regex.captures_iter(line) {
196             if let Some(error_code) = cap.get(1) {
197                 error_codes.entry(error_code.as_str().to_owned()).or_default().is_used = true;
198             }
199         }
200     }
201 }
202
203 pub fn check(paths: &[&Path], bad: &mut bool) {
204     let mut errors = Vec::new();
205     let mut found_explanations = 0;
206     let mut found_tests = 0;
207     let mut error_codes: HashMap<String, ErrorCodeStatus> = HashMap::new();
208     let mut explanations: HashSet<String> = HashSet::new();
209     // We want error codes which match the following cases:
210     //
211     // * foo(a, E0111, a)
212     // * foo(a, E0111)
213     // * foo(E0111, a)
214     // * #[error = "E0111"]
215     let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();
216
217     println!("Checking which error codes lack tests...");
218
219     for path in paths {
220         super::walk(path, &mut |path| super::filter_dirs(path), &mut |entry, contents| {
221             let file_name = entry.file_name();
222             let entry_path = entry.path();
223
224             if file_name == "error_codes.rs" {
225                 extract_error_codes(contents, &mut error_codes, entry.path(), &mut errors);
226                 found_explanations += 1;
227             } else if entry_path.extension() == Some(OsStr::new("stderr")) {
228                 extract_error_codes_from_tests(contents, &mut error_codes);
229                 found_tests += 1;
230             } else if entry_path.extension() == Some(OsStr::new("rs")) {
231                 let path = entry.path().to_string_lossy();
232                 if PATHS_TO_IGNORE_FOR_EXTRACTION.iter().all(|c| !path.contains(c)) {
233                     extract_error_codes_from_source(contents, &mut error_codes, &regex);
234                 }
235             } else if entry_path
236                 .parent()
237                 .and_then(|p| p.file_name())
238                 .map(|p| p == "error_codes")
239                 .unwrap_or(false)
240                 && entry_path.extension() == Some(OsStr::new("md"))
241             {
242                 explanations.insert(file_name.to_str().unwrap().replace(".md", ""));
243             }
244         });
245     }
246     if found_explanations == 0 {
247         eprintln!("No error code explanation was tested!");
248         *bad = true;
249     }
250     if found_tests == 0 {
251         eprintln!("No error code was found in compilation errors!");
252         *bad = true;
253     }
254     if explanations.is_empty() {
255         eprintln!("No error code explanation was found!");
256         *bad = true;
257     }
258     if errors.is_empty() {
259         println!("Found {} error codes", error_codes.len());
260
261         for (err_code, error_status) in &error_codes {
262             if !error_status.has_test && !EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
263                 errors.push(format!("Error code {err_code} needs to have at least one UI test!"));
264             } else if error_status.has_test && EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
265                 errors.push(format!(
266                     "Error code {} has a UI test, it shouldn't be listed into EXEMPTED_FROM_TEST!",
267                     err_code
268                 ));
269             }
270             if !error_status.is_used && !error_status.has_explanation {
271                 errors.push(format!(
272                     "Error code {} isn't used and doesn't have an error explanation, it should be \
273                      commented in error_codes.rs file",
274                     err_code
275                 ));
276             }
277         }
278     }
279     if errors.is_empty() {
280         // Checking if local constants need to be cleaned.
281         for err_code in EXEMPTED_FROM_TEST {
282             match error_codes.get(err_code.to_owned()) {
283                 Some(status) => {
284                     if status.has_test {
285                         errors.push(format!(
286                             "{} error code has a test and therefore should be \
287                             removed from the `EXEMPTED_FROM_TEST` constant",
288                             err_code
289                         ));
290                     }
291                 }
292                 None => errors.push(format!(
293                     "{} error code isn't used anymore and therefore should be removed \
294                         from `EXEMPTED_FROM_TEST` constant",
295                     err_code
296                 )),
297             }
298         }
299     }
300     if errors.is_empty() {
301         for explanation in explanations {
302             if !error_codes.contains_key(&explanation) {
303                 errors.push(format!(
304                     "{} error code explanation should be listed in `error_codes.rs`",
305                     explanation
306                 ));
307             }
308         }
309     }
310     errors.sort();
311     for err in &errors {
312         eprintln!("{err}");
313     }
314     println!("Found {} error(s) in error codes", errors.len());
315     if !errors.is_empty() {
316         *bad = true;
317     }
318     println!("Done!");
319 }