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::{DiagCtx, RunningCheck};
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, diag_ctx: DiagCtx) {
40    let mut check = diag_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('E') {
95            let split_line = line.split_once(':');
96
97            // Extract the error code from the line. Emit a fatal error if it is not in the correct
98            // format.
99            let Some(split_line) = split_line else {
100                check.error(format!(
101                    "{path}:{line_index}: Expected a line with the format `Eabcd: abcd, \
102                    but got \"{line}\" without a `:` delimiter",
103                ));
104                continue;
105            };
106
107            let err_code = split_line.0.to_owned();
108
109            // If this is a duplicate of another error code, emit a fatal error.
110            if error_codes.contains(&err_code) {
111                check.error(format!(
112                    "{path}:{line_index}: Found duplicate error code: `{err_code}`"
113                ));
114                continue;
115            }
116
117            let mut chars = err_code.chars();
118            assert_eq!(chars.next(), Some('E'));
119            let error_num_as_str = chars.as_str();
120
121            // Ensure that the line references the correct markdown file.
122            let rest = split_line.1.split_once(',');
123            let Some(rest) = rest else {
124                check.error(format!(
125                    "{path}:{line_index}: Expected a line with the format `Eabcd: abcd, \
126                    but got \"{line}\" without a `,` delimiter",
127                ));
128                continue;
129            };
130            if error_num_as_str != rest.0.trim() {
131                check.error(format!(
132                    "{path}:{line_index}: `{}:` should be followed by `{},` but instead found `{}` in \
133                    `compiler/rustc_error_codes/src/lib.rs`",
134                    err_code,
135                    error_num_as_str,
136                    split_line.1,
137                ));
138                continue;
139            }
140            if !rest.1.trim().is_empty() && !rest.1.trim().starts_with("//") {
141                check.error(format!("{path}:{line_index}: should only have one error per line"));
142                continue;
143            }
144
145            error_codes.push(err_code);
146        }
147    }
148
149    error_codes
150}
151
152/// Stage 2: Checks that long-form error code explanations exist and have doctests.
153fn check_error_codes_docs(
154    root_path: &Path,
155    error_codes: &[String],
156    check: &mut RunningCheck,
157) -> Vec<String> {
158    let docs_path = root_path.join(Path::new(ERROR_DOCS_PATH));
159
160    let mut no_longer_emitted_codes = Vec::new();
161
162    walk(&docs_path, |_, _| false, &mut |entry, contents| {
163        let path = entry.path();
164
165        // Error if the file isn't markdown.
166        if path.extension() != Some(OsStr::new("md")) {
167            check.error(format!(
168                "Found unexpected non-markdown file in error code docs directory: {}",
169                path.display()
170            ));
171            return;
172        }
173
174        // Make sure that the file is referenced in `rustc_error_codes/src/lib.rs`
175        let filename = path.file_name().unwrap().to_str().unwrap().split_once('.');
176        let err_code = filename.unwrap().0; // `unwrap` is ok because we know the filename is in the correct format.
177
178        if error_codes.iter().all(|e| e != err_code) {
179            check.error(format!(
180                "Found valid file `{}` in error code docs directory without corresponding \
181                entry in `rustc_error_codes/src/lib.rs`",
182                path.display()
183            ));
184            return;
185        }
186
187        let (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted) =
188            check_explanation_has_doctest(contents, err_code);
189
190        if emit_ignore_warning {
191            check.verbose_msg(format!(
192                "warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
193                `IGNORE_DOCTEST_CHECK` constant instead."
194            ));
195        }
196
197        if no_longer_emitted {
198            no_longer_emitted_codes.push(err_code.to_owned());
199        }
200
201        if !found_code_example {
202            check.verbose_msg(format!(
203                "warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
204                (even if untested)."
205            ));
206            return;
207        }
208
209        let test_ignored = IGNORE_DOCTEST_CHECK.contains(&err_code);
210
211        // Check that the explanation has a doctest, and if it shouldn't, that it doesn't
212        if !found_proper_doctest && !test_ignored {
213            check.error(format!(
214                "`{}` doesn't use its own error code in compile_fail example",
215                path.display(),
216            ));
217        } else if found_proper_doctest && test_ignored {
218            check.error(format!(
219                "`{}` has a compile_fail doctest with its own error code, it shouldn't \
220                be listed in `IGNORE_DOCTEST_CHECK`",
221                path.display(),
222            ));
223        }
224    });
225
226    no_longer_emitted_codes
227}
228
229/// This function returns a tuple indicating whether the provided explanation:
230/// a) has a code example, tested or not.
231/// b) has a valid doctest
232fn check_explanation_has_doctest(explanation: &str, err_code: &str) -> (bool, bool, bool, bool) {
233    let mut found_code_example = false;
234    let mut found_proper_doctest = false;
235
236    let mut emit_ignore_warning = false;
237    let mut no_longer_emitted = false;
238
239    for line in explanation.lines() {
240        let line = line.trim();
241
242        if line.starts_with("```") {
243            found_code_example = true;
244
245            // Check for the `rustdoc` doctest headers.
246            if line.contains("compile_fail") && line.contains(err_code) {
247                found_proper_doctest = true;
248            }
249
250            if line.contains("ignore") {
251                emit_ignore_warning = true;
252                found_proper_doctest = true;
253            }
254        } else if line
255            .starts_with("#### Note: this error code is no longer emitted by the compiler")
256        {
257            no_longer_emitted = true;
258            found_code_example = true;
259            found_proper_doctest = true;
260        }
261    }
262
263    (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted)
264}
265
266// Stage 3: Checks that each error code has a UI test in the correct directory
267fn check_error_codes_tests(
268    root_path: &Path,
269    error_codes: &[String],
270    check: &mut RunningCheck,
271    no_longer_emitted: &[String],
272) {
273    let tests_path = root_path.join(Path::new(ERROR_TESTS_PATH));
274
275    for code in error_codes {
276        let test_path = tests_path.join(format!("{code}.stderr"));
277
278        if !test_path.exists() && !IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
279            check.verbose_msg(format!(
280                "warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
281            ));
282            continue;
283        }
284        if IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
285            if test_path.exists() {
286                check.error(format!(
287                    "Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
288                ));
289            }
290            continue;
291        }
292
293        let file = match fs::read_to_string(&test_path) {
294            Ok(file) => file,
295            Err(err) => {
296                check.verbose_msg(format!(
297                    "warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
298                    test_path.display()
299                ));
300                continue;
301            }
302        };
303
304        if no_longer_emitted.contains(code) {
305            // UI tests *can't* contain error codes that are no longer emitted.
306            continue;
307        }
308
309        let mut found_code = false;
310
311        for line in file.lines() {
312            let s = line.trim();
313            // Assuming the line starts with `error[E`, we can substring the error code out.
314            if s.starts_with("error[E") && &s[6..11] == code {
315                found_code = true;
316                break;
317            };
318        }
319
320        if !found_code {
321            check.verbose_msg(format!(
322                "warning: Error code `{code}` has a UI test file, but doesn't contain its own error code!"
323            ));
324        }
325    }
326}
327
328/// Stage 4: Search `compiler/` and ensure that every error code is actually used by the compiler and that no undocumented error codes exist.
329fn check_error_codes_used(
330    search_paths: &[&Path],
331    error_codes: &[String],
332    check: &mut RunningCheck,
333    no_longer_emitted: &[String],
334) {
335    // Search for error codes in the form `E0123`.
336    let regex = Regex::new(r#"\bE\d{4}\b"#).unwrap();
337
338    let mut found_codes = Vec::new();
339
340    walk_many(search_paths, |path, _is_dir| filter_dirs(path), &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(0) {
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                        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`."));
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            check.error(format!(
374                "Error code `{code}` exists, but is not emitted by the compiler!\n\
375                Please mark the code as no longer emitted by adding the following note to the top of the `EXXXX.md` file:\n\
376                `#### Note: this error code is no longer emitted by the compiler`\n\
377                Also, do not forget to mark doctests that no longer apply as `ignore (error is no longer emitted)`."
378            ));
379        }
380
381        if found_codes.contains(code) && no_longer_emitted.contains(code) {
382            check.verbose_msg(format!(
383                "warning: Error code `{code}` is used when it's marked as \"no longer emitted\""
384            ));
385        }
386    }
387}