blob: 6fc65e56901a74dffcb127b66b7cb7df3210731e [file] [log] [blame]
//! Tidy check to ensure error codes are properly documented and tested.
//!
//! Overview of check:
//!
//! 1. We create a list of error codes used by the compiler. Error codes are extracted from `compiler/rustc_error_codes/src/lib.rs`.
//!
//! 2. We check that the error code has a long-form explanation in `compiler/rustc_error_codes/src/error_codes/`.
//! - The explanation is expected to contain a `doctest` that fails with the correct error code. (`EXEMPT_FROM_DOCTEST` *currently* bypasses this check)
//! - Note that other stylistic conventions for markdown files are checked in the `style.rs` tidy check.
//!
//! 3. We check that the error code has a UI test in `tests/ui/error-codes/`.
//! - We ensure that there is both a `Exxxx.rs` file and a corresponding `Exxxx.stderr` file.
//! - We also ensure that the error code is used in the tests.
//! - *Currently*, it is possible to opt-out of this check with the `EXEMPTED_FROM_TEST` constant.
//!
//! 4. We check that the error code is actually emitted by the compiler.
//! - This is done by searching `compiler/` with a regex.
use std::{ffi::OsStr, fs, path::Path};
use regex::Regex;
use crate::walk::{filter_dirs, walk, walk_many};
const ERROR_CODES_PATH: &str = "compiler/rustc_error_codes/src/lib.rs";
const ERROR_DOCS_PATH: &str = "compiler/rustc_error_codes/src/error_codes/";
const ERROR_TESTS_PATH: &str = "tests/ui/error-codes/";
// 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.
const IGNORE_DOCTEST_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0717"];
// Error codes that don't yet have a UI test. This list will eventually be removed.
const IGNORE_UI_TEST_CHECK: &[&str] =
&["E0461", "E0465", "E0514", "E0554", "E0640", "E0717", "E0729"];
macro_rules! verbose_print {
($verbose:expr, $($fmt:tt)*) => {
if $verbose {
println!("{}", format_args!($($fmt)*));
}
};
}
pub fn check(root_path: &Path, search_paths: &[&Path], verbose: bool, bad: &mut bool) {
let mut errors = Vec::new();
// Stage 1: create list
let error_codes = extract_error_codes(root_path, &mut errors);
if verbose {
println!("Found {} error codes", error_codes.len());
println!("Highest error code: `{}`", error_codes.iter().max().unwrap());
}
// Stage 2: check list has docs
let no_longer_emitted = check_error_codes_docs(root_path, &error_codes, &mut errors, verbose);
// Stage 3: check list has UI tests
check_error_codes_tests(root_path, &error_codes, &mut errors, verbose, &no_longer_emitted);
// Stage 4: check list is emitted by compiler
check_error_codes_used(search_paths, &error_codes, &mut errors, &no_longer_emitted, verbose);
// Print any errors.
for error in errors {
tidy_error!(bad, "{}", error);
}
}
/// Stage 1: Parses a list of error codes from `error_codes.rs`.
fn extract_error_codes(root_path: &Path, errors: &mut Vec<String>) -> Vec<String> {
let path = root_path.join(Path::new(ERROR_CODES_PATH));
let file =
fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read `{path:?}`: {e}"));
let mut error_codes = Vec::new();
for line in file.lines() {
let line = line.trim();
if line.starts_with('E') {
let split_line = line.split_once(':');
// Extract the error code from the line. Emit a fatal error if it is not in the correct
// format.
let err_code = if let Some(err_code) = split_line {
err_code.0.to_owned()
} else {
errors.push(format!(
"Expected a line with the format `Eabcd: abcd, \
but got \"{}\" without a `:` delimiter",
line,
));
continue;
};
// If this is a duplicate of another error code, emit a fatal error.
if error_codes.contains(&err_code) {
errors.push(format!("Found duplicate error code: `{}`", err_code));
continue;
}
let mut chars = err_code.chars();
chars.next();
let error_num_as_str = chars.as_str();
// Ensure that the line references the correct markdown file.
let expected_filename = format!(" {},", error_num_as_str);
if expected_filename != split_line.unwrap().1 {
errors.push(format!(
"`{}:` should be followed by `{}` but instead found `{}` in \
`compiler/rustc_error_codes/src/lib.rs`",
err_code,
expected_filename,
split_line.unwrap().1,
));
continue;
}
error_codes.push(err_code);
}
}
error_codes
}
/// Stage 2: Checks that long-form error code explanations exist and have doctests.
fn check_error_codes_docs(
root_path: &Path,
error_codes: &[String],
errors: &mut Vec<String>,
verbose: bool,
) -> Vec<String> {
let docs_path = root_path.join(Path::new(ERROR_DOCS_PATH));
let mut no_longer_emitted_codes = Vec::new();
walk(&docs_path, |_, _| false, &mut |entry, contents| {
let path = entry.path();
// Error if the file isn't markdown.
if path.extension() != Some(OsStr::new("md")) {
errors.push(format!(
"Found unexpected non-markdown file in error code docs directory: {}",
path.display()
));
return;
}
// Make sure that the file is referenced in `error_codes.rs`
let filename = path.file_name().unwrap().to_str().unwrap().split_once('.');
let err_code = filename.unwrap().0; // `unwrap` is ok because we know the filename is in the correct format.
if error_codes.iter().all(|e| e != err_code) {
errors.push(format!(
"Found valid file `{}` in error code docs directory without corresponding \
entry in `error_code.rs`",
path.display()
));
return;
}
let (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted) =
check_explanation_has_doctest(&contents, &err_code);
if emit_ignore_warning {
verbose_print!(
verbose,
"warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
`IGNORE_DOCTEST_CHECK` constant instead."
);
}
if no_longer_emitted {
no_longer_emitted_codes.push(err_code.to_owned());
}
if !found_code_example {
verbose_print!(
verbose,
"warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
(even if untested)."
);
return;
}
let test_ignored = IGNORE_DOCTEST_CHECK.contains(&&err_code);
// Check that the explanation has a doctest, and if it shouldn't, that it doesn't
if !found_proper_doctest && !test_ignored {
errors.push(format!(
"`{}` doesn't use its own error code in compile_fail example",
path.display(),
));
} else if found_proper_doctest && test_ignored {
errors.push(format!(
"`{}` has a compile_fail doctest with its own error code, it shouldn't \
be listed in `IGNORE_DOCTEST_CHECK`",
path.display(),
));
}
});
no_longer_emitted_codes
}
/// This function returns a tuple indicating whether the provided explanation:
/// a) has a code example, tested or not.
/// b) has a valid doctest
fn check_explanation_has_doctest(explanation: &str, err_code: &str) -> (bool, bool, bool, bool) {
let mut found_code_example = false;
let mut found_proper_doctest = false;
let mut emit_ignore_warning = false;
let mut no_longer_emitted = false;
for line in explanation.lines() {
let line = line.trim();
if line.starts_with("```") {
found_code_example = true;
// Check for the `rustdoc` doctest headers.
if line.contains("compile_fail") && line.contains(err_code) {
found_proper_doctest = true;
}
if line.contains("ignore") {
emit_ignore_warning = true;
found_proper_doctest = true;
}
} else if line
.starts_with("#### Note: this error code is no longer emitted by the compiler")
{
no_longer_emitted = true;
found_code_example = true;
found_proper_doctest = true;
}
}
(found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted)
}
// Stage 3: Checks that each error code has a UI test in the correct directory
fn check_error_codes_tests(
root_path: &Path,
error_codes: &[String],
errors: &mut Vec<String>,
verbose: bool,
no_longer_emitted: &[String],
) {
let tests_path = root_path.join(Path::new(ERROR_TESTS_PATH));
for code in error_codes {
let test_path = tests_path.join(format!("{}.stderr", code));
if !test_path.exists() && !IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
verbose_print!(
verbose,
"warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
);
continue;
}
if IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
if test_path.exists() {
errors.push(format!(
"Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
));
}
continue;
}
let file = match fs::read_to_string(&test_path) {
Ok(file) => file,
Err(err) => {
verbose_print!(
verbose,
"warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
test_path.display()
);
continue;
}
};
if no_longer_emitted.contains(code) {
// UI tests *can't* contain error codes that are no longer emitted.
continue;
}
let mut found_code = false;
for line in file.lines() {
let s = line.trim();
// Assuming the line starts with `error[E`, we can substring the error code out.
if s.starts_with("error[E") {
if &s[6..11] == code {
found_code = true;
break;
}
};
}
if !found_code {
verbose_print!(
verbose,
"warning: Error code {code}`` has a UI test file, but doesn't contain its own error code!"
);
}
}
}
/// Stage 4: Search `compiler/` and ensure that every error code is actually used by the compiler and that no undocumented error codes exist.
fn check_error_codes_used(
search_paths: &[&Path],
error_codes: &[String],
errors: &mut Vec<String>,
no_longer_emitted: &[String],
verbose: bool,
) {
// Search for error codes in the form `E0123`.
let regex = Regex::new(r#"\bE\d{4}\b"#).unwrap();
let mut found_codes = Vec::new();
walk_many(search_paths, |path, _is_dir| filter_dirs(path), &mut |entry, contents| {
let path = entry.path();
// Return early if we aren't looking at a source file.
if path.extension() != Some(OsStr::new("rs")) {
return;
}
for line in contents.lines() {
// We want to avoid parsing error codes in comments.
if line.trim_start().starts_with("//") {
continue;
}
for cap in regex.captures_iter(line) {
if let Some(error_code) = cap.get(0) {
let error_code = error_code.as_str().to_owned();
if !error_codes.contains(&error_code) {
// This error code isn't properly defined, we must error.
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));
continue;
}
// This error code can now be marked as used.
found_codes.push(error_code);
}
}
}
});
for code in error_codes {
if !found_codes.contains(code) && !no_longer_emitted.contains(code) {
errors.push(format!(
"Error code `{code}` exists, but is not emitted by the compiler!\n\
Please mark the code as no longer emitted by adding the following note to the top of the `EXXXX.md` file:\n\
`#### Note: this error code is no longer emitted by the compiler`\n\
Also, do not forget to mark doctests that no longer apply as `ignore (error is no longer emitted)`."
));
}
if found_codes.contains(code) && no_longer_emitted.contains(code) {
verbose_print!(
verbose,
"warning: Error code `{code}` is used when it's marked as \"no longer emitted\""
);
}
}
}