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