1 //! Tidy check to ensure error codes are properly documented and tested.
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`.
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.
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.
16 //! 4. We check that the error code is actually emitted by the compiler.
17 //! - This is done by searching `compiler/` with a regex.
19 use std
::{ffi::OsStr, fs, path::Path}
;
23 use crate::walk
::{filter_dirs, walk, walk_many}
;
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/";
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"];
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", "E0514", "E0554", "E0640", "E0717", "E0729"];
36 macro_rules
! verbose_print
{
37 ($verbose
:expr
, $
($fmt
:tt
)*) => {
39 println
!("{}", format_args
!($
($fmt
)*));
44 pub fn check(root_path
: &Path
, search_paths
: &[&Path
], verbose
: bool
, bad
: &mut bool
) {
45 let mut errors
= Vec
::new();
47 // Stage 1: create list
48 let error_codes
= extract_error_codes(root_path
, &mut errors
);
50 println
!("Found {} error codes", error_codes
.len());
51 println
!("Highest error code: `{}`", error_codes
.iter().max().unwrap());
54 // Stage 2: check list has docs
55 let no_longer_emitted
= check_error_codes_docs(root_path
, &error_codes
, &mut errors
, verbose
);
57 // Stage 3: check list has UI tests
58 check_error_codes_tests(root_path
, &error_codes
, &mut errors
, verbose
, &no_longer_emitted
);
60 // Stage 4: check list is emitted by compiler
61 check_error_codes_used(search_paths
, &error_codes
, &mut errors
, &no_longer_emitted
, verbose
);
65 tidy_error
!(bad
, "{}", error
);
69 /// Stage 1: Parses a list of error codes from `error_codes.rs`.
70 fn extract_error_codes(root_path
: &Path
, errors
: &mut Vec
<String
>) -> Vec
<String
> {
71 let path
= root_path
.join(Path
::new(ERROR_CODES_PATH
));
73 fs
::read_to_string(&path
).unwrap_or_else(|e
| panic
!("failed to read `{path:?}`: {e}"));
75 let mut error_codes
= Vec
::new();
77 for line
in file
.lines() {
78 let line
= line
.trim();
80 if line
.starts_with('E'
) {
81 let split_line
= line
.split_once('
:'
);
83 // Extract the error code from the line, emitting a fatal error if it is not in a correct format.
84 let err_code
= if let Some(err_code
) = split_line
{
88 "Expected a line with the format `Exxxx: include_str!(\"..\")`, but got \"{}\" \
89 without a `:` delimiter",
95 // If this is a duplicate of another error code, emit a fatal error.
96 if error_codes
.contains(&err_code
) {
97 errors
.push(format
!("Found duplicate error code: `{}`", err_code
));
101 // Ensure that the line references the correct markdown file.
102 let expected_filename
= format
!(" include_str!(\"./error_codes/{}.md\"),", err_code
);
103 if expected_filename
!= split_line
.unwrap().1 {
105 "Error code `{}` expected to reference docs with `{}` but instead found `{}` in \
106 `compiler/rustc_error_codes/src/error_codes.rs`",
109 split_line
.unwrap().1,
114 error_codes
.push(err_code
);
121 /// Stage 2: Checks that long-form error code explanations exist and have doctests.
122 fn check_error_codes_docs(
124 error_codes
: &[String
],
125 errors
: &mut Vec
<String
>,
128 let docs_path
= root_path
.join(Path
::new(ERROR_DOCS_PATH
));
130 let mut no_longer_emitted_codes
= Vec
::new();
132 walk(&docs_path
, |_
, _
| false, &mut |entry
, contents
| {
133 let path
= entry
.path();
135 // Error if the file isn't markdown.
136 if path
.extension() != Some(OsStr
::new("md")) {
138 "Found unexpected non-markdown file in error code docs directory: {}",
144 // Make sure that the file is referenced in `error_codes.rs`
145 let filename
= path
.file_name().unwrap().to_str().unwrap().split_once('
.'
);
146 let err_code
= filename
.unwrap().0; // `unwrap` is ok because we know the filename is in the correct format.
148 if error_codes
.iter().all(|e
| e
!= err_code
) {
150 "Found valid file `{}` in error code docs directory without corresponding \
151 entry in `error_code.rs`",
157 let (found_code_example
, found_proper_doctest
, emit_ignore_warning
, no_longer_emitted
) =
158 check_explanation_has_doctest(&contents
, &err_code
);
160 if emit_ignore_warning
{
163 "warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
164 `IGNORE_DOCTEST_CHECK` constant instead."
168 if no_longer_emitted
{
169 no_longer_emitted_codes
.push(err_code
.to_owned());
172 if !found_code_example
{
175 "warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
181 let test_ignored
= IGNORE_DOCTEST_CHECK
.contains(&&err_code
);
183 // Check that the explanation has a doctest, and if it shouldn't, that it doesn't
184 if !found_proper_doctest
&& !test_ignored
{
186 "`{}` doesn't use its own error code in compile_fail example",
189 } else if found_proper_doctest
&& test_ignored
{
191 "`{}` has a compile_fail doctest with its own error code, it shouldn't \
192 be listed in `IGNORE_DOCTEST_CHECK`",
198 no_longer_emitted_codes
201 /// This function returns a tuple indicating whether the provided explanation:
202 /// a) has a code example, tested or not.
203 /// b) has a valid doctest
204 fn check_explanation_has_doctest(explanation
: &str, err_code
: &str) -> (bool
, bool
, bool
, bool
) {
205 let mut found_code_example
= false;
206 let mut found_proper_doctest
= false;
208 let mut emit_ignore_warning
= false;
209 let mut no_longer_emitted
= false;
211 for line
in explanation
.lines() {
212 let line
= line
.trim();
214 if line
.starts_with("```") {
215 found_code_example
= true;
217 // Check for the `rustdoc` doctest headers.
218 if line
.contains("compile_fail") && line
.contains(err_code
) {
219 found_proper_doctest
= true;
222 if line
.contains("ignore") {
223 emit_ignore_warning
= true;
224 found_proper_doctest
= true;
227 .starts_with("#### Note: this error code is no longer emitted by the compiler")
229 no_longer_emitted
= true;
230 found_code_example
= true;
231 found_proper_doctest
= true;
235 (found_code_example
, found_proper_doctest
, emit_ignore_warning
, no_longer_emitted
)
238 // Stage 3: Checks that each error code has a UI test in the correct directory
239 fn check_error_codes_tests(
241 error_codes
: &[String
],
242 errors
: &mut Vec
<String
>,
244 no_longer_emitted
: &[String
],
246 let tests_path
= root_path
.join(Path
::new(ERROR_TESTS_PATH
));
248 for code
in error_codes
{
249 let test_path
= tests_path
.join(format
!("{}.stderr", code
));
251 if !test_path
.exists() && !IGNORE_UI_TEST_CHECK
.contains(&code
.as_str()) {
254 "warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
258 if IGNORE_UI_TEST_CHECK
.contains(&code
.as_str()) {
259 if test_path
.exists() {
261 "Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
267 let file
= match fs
::read_to_string(&test_path
) {
272 "warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
279 if no_longer_emitted
.contains(code
) {
280 // UI tests *can't* contain error codes that are no longer emitted.
284 let mut found_code
= false;
286 for line
in file
.lines() {
288 // Assuming the line starts with `error[E`, we can substring the error code out.
289 if s
.starts_with("error[E") {
290 if &s
[6..11] == code
{
300 "warning: Error code {code}`` has a UI test file, but doesn't contain its own error code!"
306 /// Stage 4: Search `compiler/` and ensure that every error code is actually used by the compiler and that no undocumented error codes exist.
307 fn check_error_codes_used(
308 search_paths
: &[&Path
],
309 error_codes
: &[String
],
310 errors
: &mut Vec
<String
>,
311 no_longer_emitted
: &[String
],
314 // We want error codes which match the following cases:
316 // * foo(a, E0111, a)
319 // * #[error = "E0111"]
320 let regex
= Regex
::new(r
#"[(,"\s](E\d{4})[,)"]"#).unwrap();
322 let mut found_codes
= Vec
::new();
324 walk_many(search_paths
, |path
, _is_dir
| filter_dirs(path
), &mut |entry
, contents
| {
325 let path
= entry
.path();
327 // Return early if we aren't looking at a source file.
328 if path
.extension() != Some(OsStr
::new("rs")) {
332 for line
in contents
.lines() {
333 // We want to avoid parsing error codes in comments.
334 if line
.trim_start().starts_with("//") {
338 for cap
in regex
.captures_iter(line
) {
339 if let Some(error_code
) = cap
.get(1) {
340 let error_code
= error_code
.as_str().to_owned();
342 if !error_codes
.contains(&error_code
) {
343 // This error code isn't properly defined, we must error.
344 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
));
348 // This error code can now be marked as used.
349 found_codes
.push(error_code
);
355 for code
in error_codes
{
356 if !found_codes
.contains(code
) && !no_longer_emitted
.contains(code
) {
358 "Error code `{code}` exists, but is not emitted by the compiler!\n\
359 Please mark the code as no longer emitted by adding the following note to the top of the `EXXXX.md` file:\n\
360 `#### Note: this error code is no longer emitted by the compiler`\n\
361 Also, do not forget to mark doctests that no longer apply as `ignore (error is no longer emitted)`."
365 if found_codes
.contains(code
) && no_longer_emitted
.contains(code
) {
368 "warning: Error code `{code}` is used when it's marked as \"no longer emitted\""