]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/error_codes.rs
Remove astconv usage in diagnostic
[rust.git] / src / tools / tidy / src / error_codes.rs
1 //! Tidy check to ensure error codes are properly documented and tested.
2 //!
3 //! Overview of check:
4 //!
5 //! 1. We create a list of error codes used by the compiler. Error codes are extracted from `compiler/rustc_error_codes/src/error_codes.rs`.
6 //!
7 //! 2. We check that the error code has a long-form explanation in `compiler/rustc_error_codes/src/error_codes/`.
8 //!   - The explanation is expected to contain a `doctest` that fails with the correct error code. (`EXEMPT_FROM_DOCTEST` *currently* bypasses this check)
9 //!   - Note that other stylistic conventions for markdown files are checked in the `style.rs` tidy check.
10 //!
11 //! 3. We check that the error code has a UI test in `tests/ui/error-codes/`.
12 //!   - We ensure that there is both a `Exxxx.rs` file and a corresponding `Exxxx.stderr` file.
13 //!   - We also ensure that the error code is used in the tests.
14 //!   - *Currently*, it is possible to opt-out of this check with the `EXEMPTED_FROM_TEST` constant.
15 //!
16 //! 4. We check that the error code is actually emitted by the compiler.
17 //!   - This is done by searching `compiler/` with a regex.
18
19 use std::{ffi::OsStr, fs, path::Path};
20
21 use regex::Regex;
22
23 use crate::walk::{filter_dirs, walk, walk_many};
24
25 const ERROR_CODES_PATH: &str = "compiler/rustc_error_codes/src/error_codes.rs";
26 const ERROR_DOCS_PATH: &str = "compiler/rustc_error_codes/src/error_codes/";
27 const ERROR_TESTS_PATH: &str = "tests/ui/error-codes/";
28
29 // Error codes that (for some reason) can't have a doctest in their explanation. Error codes are still expected to provide a code example, even if untested.
30 const IGNORE_DOCTEST_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0640", "E0717"];
31
32 // Error codes that don't yet have a UI test. This list will eventually be removed.
33 const IGNORE_UI_TEST_CHECK: &[&str] =
34     &["E0461", "E0465", "E0476", "E0514", "E0523", "E0554", "E0640", "E0717", "E0729"];
35
36 macro_rules! verbose_print {
37     ($verbose:expr, $($fmt:tt)*) => {
38         if $verbose {
39             println!("{}", format_args!($($fmt)*));
40         }
41     };
42 }
43
44 pub fn check(root_path: &Path, search_paths: &[&Path], verbose: bool, bad: &mut bool) {
45     let mut errors = Vec::new();
46
47     // Stage 1: create list
48     let error_codes = extract_error_codes(root_path, &mut errors, verbose);
49     println!("Found {} error codes", error_codes.len());
50     println!("Highest error code: `{}`", error_codes.iter().max().unwrap());
51
52     // Stage 2: check list has docs
53     let no_longer_emitted = check_error_codes_docs(root_path, &error_codes, &mut errors, verbose);
54
55     // Stage 3: check list has UI tests
56     check_error_codes_tests(root_path, &error_codes, &mut errors, verbose, &no_longer_emitted);
57
58     // Stage 4: check list is emitted by compiler
59     check_error_codes_used(search_paths, &error_codes, &mut errors, &no_longer_emitted, verbose);
60
61     // Print any errors.
62     for error in errors {
63         tidy_error!(bad, "{}", error);
64     }
65 }
66
67 /// Stage 1: Parses a list of error codes from `error_codes.rs`.
68 fn extract_error_codes(root_path: &Path, errors: &mut Vec<String>, verbose: bool) -> Vec<String> {
69     let path = root_path.join(Path::new(ERROR_CODES_PATH));
70     let file =
71         fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read `{path:?}`: {e}"));
72
73     let mut error_codes = Vec::new();
74     let mut reached_undocumented_codes = false;
75
76     for line in file.lines() {
77         let line = line.trim();
78
79         if !reached_undocumented_codes && line.starts_with('E') {
80             let split_line = line.split_once(':');
81
82             // Extract the error code from the line, emitting a fatal error if it is not in a correct format.
83             let err_code = if let Some(err_code) = split_line {
84                 err_code.0.to_owned()
85             } else {
86                 errors.push(format!(
87                     "Expected a line with the format `Exxxx: include_str!(\"..\")`, but got \"{}\" \
88                     without a `:` delimiter",
89                     line,
90                 ));
91                 continue;
92             };
93
94             // If this is a duplicate of another error code, emit a fatal error.
95             if error_codes.contains(&err_code) {
96                 errors.push(format!("Found duplicate error code: `{}`", err_code));
97                 continue;
98             }
99
100             // Ensure that the line references the correct markdown file.
101             let expected_filename = format!(" include_str!(\"./error_codes/{}.md\"),", err_code);
102             if expected_filename != split_line.unwrap().1 {
103                 errors.push(format!(
104                     "Error code `{}` expected to reference docs with `{}` but instead found `{}` in \
105                     `compiler/rustc_error_codes/src/error_codes.rs`",
106                     err_code,
107                     expected_filename,
108                     split_line.unwrap().1,
109                 ));
110                 continue;
111             }
112
113             error_codes.push(err_code);
114         } else if reached_undocumented_codes && line.starts_with('E') {
115             let err_code = match line.split_once(',') {
116                 None => line,
117                 Some((err_code, _)) => err_code,
118             }
119             .to_string();
120
121             verbose_print!(verbose, "warning: Error code `{}` is undocumented.", err_code);
122
123             if error_codes.contains(&err_code) {
124                 errors.push(format!("Found duplicate error code: `{}`", err_code));
125             }
126
127             error_codes.push(err_code);
128         } else if line == ";" {
129             // Once we reach the undocumented error codes, adapt to different syntax.
130             reached_undocumented_codes = true;
131         }
132     }
133
134     error_codes
135 }
136
137 /// Stage 2: Checks that long-form error code explanations exist and have doctests.
138 fn check_error_codes_docs(
139     root_path: &Path,
140     error_codes: &[String],
141     errors: &mut Vec<String>,
142     verbose: bool,
143 ) -> Vec<String> {
144     let docs_path = root_path.join(Path::new(ERROR_DOCS_PATH));
145
146     let mut no_longer_emitted_codes = Vec::new();
147
148     walk(&docs_path, &mut |_| false, &mut |entry, contents| {
149         let path = entry.path();
150
151         // Error if the file isn't markdown.
152         if path.extension() != Some(OsStr::new("md")) {
153             errors.push(format!(
154                 "Found unexpected non-markdown file in error code docs directory: {}",
155                 path.display()
156             ));
157             return;
158         }
159
160         // Make sure that the file is referenced in `error_codes.rs`
161         let filename = path.file_name().unwrap().to_str().unwrap().split_once('.');
162         let err_code = filename.unwrap().0; // `unwrap` is ok because we know the filename is in the correct format.
163
164         if error_codes.iter().all(|e| e != err_code) {
165             errors.push(format!(
166                 "Found valid file `{}` in error code docs directory without corresponding \
167                 entry in `error_code.rs`",
168                 path.display()
169             ));
170             return;
171         }
172
173         let (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted) =
174             check_explanation_has_doctest(&contents, &err_code);
175
176         if emit_ignore_warning {
177             verbose_print!(
178                 verbose,
179                 "warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
180                 `IGNORE_DOCTEST_CHECK` constant instead."
181             );
182         }
183
184         if no_longer_emitted {
185             no_longer_emitted_codes.push(err_code.to_owned());
186         }
187
188         if !found_code_example {
189             verbose_print!(
190                 verbose,
191                 "warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
192                 (even if untested)."
193             );
194             return;
195         }
196
197         let test_ignored = IGNORE_DOCTEST_CHECK.contains(&&err_code);
198
199         // Check that the explanation has a doctest, and if it shouldn't, that it doesn't
200         if !found_proper_doctest && !test_ignored {
201             errors.push(format!(
202                 "`{}` doesn't use its own error code in compile_fail example",
203                 path.display(),
204             ));
205         } else if found_proper_doctest && test_ignored {
206             errors.push(format!(
207                 "`{}` has a compile_fail doctest with its own error code, it shouldn't \
208                 be listed in `IGNORE_DOCTEST_CHECK`",
209                 path.display(),
210             ));
211         }
212     });
213
214     no_longer_emitted_codes
215 }
216
217 /// This function returns a tuple indicating whether the provided explanation:
218 /// a) has a code example, tested or not.
219 /// b) has a valid doctest
220 fn check_explanation_has_doctest(explanation: &str, err_code: &str) -> (bool, bool, bool, bool) {
221     let mut found_code_example = false;
222     let mut found_proper_doctest = false;
223
224     let mut emit_ignore_warning = false;
225     let mut no_longer_emitted = false;
226
227     for line in explanation.lines() {
228         let line = line.trim();
229
230         if line.starts_with("```") {
231             found_code_example = true;
232
233             // Check for the `rustdoc` doctest headers.
234             if line.contains("compile_fail") && line.contains(err_code) {
235                 found_proper_doctest = true;
236             }
237
238             if line.contains("ignore") {
239                 emit_ignore_warning = true;
240                 found_proper_doctest = true;
241             }
242         } else if line
243             .starts_with("#### Note: this error code is no longer emitted by the compiler")
244         {
245             no_longer_emitted = true;
246             found_code_example = true;
247             found_proper_doctest = true;
248         }
249     }
250
251     (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted)
252 }
253
254 // Stage 3: Checks that each error code has a UI test in the correct directory
255 fn check_error_codes_tests(
256     root_path: &Path,
257     error_codes: &[String],
258     errors: &mut Vec<String>,
259     verbose: bool,
260     no_longer_emitted: &[String],
261 ) {
262     let tests_path = root_path.join(Path::new(ERROR_TESTS_PATH));
263
264     for code in error_codes {
265         let test_path = tests_path.join(format!("{}.stderr", code));
266
267         if !test_path.exists() && !IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
268             verbose_print!(
269                 verbose,
270                 "warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
271             );
272             continue;
273         }
274         if IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
275             if test_path.exists() {
276                 errors.push(format!(
277                     "Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
278                 ));
279             }
280             continue;
281         }
282
283         let file = match fs::read_to_string(&test_path) {
284             Ok(file) => file,
285             Err(err) => {
286                 verbose_print!(
287                     verbose,
288                     "warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
289                     test_path.display()
290                 );
291                 continue;
292             }
293         };
294
295         if no_longer_emitted.contains(code) {
296             // UI tests *can't* contain error codes that are no longer emitted.
297             continue;
298         }
299
300         let mut found_code = false;
301
302         for line in file.lines() {
303             let s = line.trim();
304             // Assuming the line starts with `error[E`, we can substring the error code out.
305             if s.starts_with("error[E") {
306                 if &s[6..11] == code {
307                     found_code = true;
308                     break;
309                 }
310             };
311         }
312
313         if !found_code {
314             verbose_print!(
315                 verbose,
316                 "warning: Error code {code}`` has a UI test file, but doesn't contain its own error code!"
317             );
318         }
319     }
320 }
321
322 /// Stage 4: Search `compiler/` and ensure that every error code is actually used by the compiler and that no undocumented error codes exist.
323 fn check_error_codes_used(
324     search_paths: &[&Path],
325     error_codes: &[String],
326     errors: &mut Vec<String>,
327     no_longer_emitted: &[String],
328     verbose: bool,
329 ) {
330     // We want error codes which match the following cases:
331     //
332     // * foo(a, E0111, a)
333     // * foo(a, E0111)
334     // * foo(E0111, a)
335     // * #[error = "E0111"]
336     let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();
337
338     let mut found_codes = Vec::new();
339
340     walk_many(search_paths, &mut filter_dirs, &mut |entry, contents| {
341         let path = entry.path();
342
343         // Return early if we aren't looking at a source file.
344         if path.extension() != Some(OsStr::new("rs")) {
345             return;
346         }
347
348         for line in contents.lines() {
349             // We want to avoid parsing error codes in comments.
350             if line.trim_start().starts_with("//") {
351                 continue;
352             }
353
354             for cap in regex.captures_iter(line) {
355                 if let Some(error_code) = cap.get(1) {
356                     let error_code = error_code.as_str().to_owned();
357
358                     if !error_codes.contains(&error_code) {
359                         // This error code isn't properly defined, we must error.
360                         errors.push(format!("Error code `{}` is used in the compiler but not defined and documented in `compiler/rustc_error_codes/src/error_codes.rs`.", error_code));
361                         continue;
362                     }
363
364                     // This error code can now be marked as used.
365                     found_codes.push(error_code);
366                 }
367             }
368         }
369     });
370
371     for code in error_codes {
372         if !found_codes.contains(code) && !no_longer_emitted.contains(code) {
373             errors.push(format!("Error code `{code}` exists, but is not emitted by the compiler!"))
374         }
375
376         if found_codes.contains(code) && no_longer_emitted.contains(code) {
377             verbose_print!(
378                 verbose,
379                 "warning: Error code `{code}` is used when it's marked as \"no longer emitted\""
380             );
381         }
382     }
383 }