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