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