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