blob: 610e322e12963e63854e0b3ba08b5300762abacd [file] [log] [blame]
//! Checks that all error codes have at least one test to prevent having error
//! codes that are silently not thrown by the compiler anymore.
use crate::walk::{filter_dirs, walk};
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs::read_to_string;
use std::path::Path;
use regex::Regex;
// A few of those error codes can't be tested but all the others can and *should* be tested!
const EXEMPTED_FROM_TEST: &[&str] = &[
"E0313", "E0377", "E0461", "E0462", "E0465", "E0476", "E0490", "E0514", "E0519", "E0523",
"E0554", "E0640", "E0717", "E0729", "E0789",
];
// Some error codes don't have any tests apparently...
const IGNORE_EXPLANATION_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0729"];
// If the file path contains any of these, we don't want to try to extract error codes from it.
//
// We need to declare each path in the windows version (with backslash).
const PATHS_TO_IGNORE_FOR_EXTRACTION: &[&str] =
&["src/test/", "src\\test\\", "src/doc/", "src\\doc\\", "src/tools/", "src\\tools\\"];
#[derive(Default, Debug)]
struct ErrorCodeStatus {
has_test: bool,
has_explanation: bool,
is_used: bool,
}
fn check_error_code_explanation(
f: &str,
error_codes: &mut HashMap<String, ErrorCodeStatus>,
err_code: String,
) -> bool {
let mut invalid_compile_fail_format = false;
let mut found_error_code = false;
for line in f.lines() {
let s = line.trim();
if s.starts_with("```") {
if s.contains("compile_fail") && s.contains('E') {
if !found_error_code {
error_codes.get_mut(&err_code).map(|x| x.has_test = true);
found_error_code = true;
}
} else if s.contains("compile-fail") {
invalid_compile_fail_format = true;
}
} else if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
if !found_error_code {
error_codes.get_mut(&err_code).map(|x| x.has_test = true);
found_error_code = true;
}
}
}
invalid_compile_fail_format
}
fn check_if_error_code_is_test_in_explanation(f: &str, err_code: &str) -> bool {
let mut ignore_found = false;
for line in f.lines() {
let s = line.trim();
if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
return true;
}
if s.starts_with("```") {
if s.contains("compile_fail") && s.contains(err_code) {
return true;
} else if s.contains("ignore") {
// It's very likely that we can't actually make it fail compilation...
ignore_found = true;
}
}
}
ignore_found
}
macro_rules! some_or_continue {
($e:expr) => {
match $e {
Some(e) => e,
None => continue,
}
};
}
fn extract_error_codes(
f: &str,
error_codes: &mut HashMap<String, ErrorCodeStatus>,
path: &Path,
errors: &mut Vec<String>,
) {
let mut reached_no_explanation = false;
for line in f.lines() {
let s = line.trim();
if !reached_no_explanation && s.starts_with('E') && s.contains("include_str!(\"") {
let err_code = s
.split_once(':')
.expect(
format!(
"Expected a line with the format `E0xxx: include_str!(\"..\")`, but got {} \
without a `:` delimiter",
s,
)
.as_str(),
)
.0
.to_owned();
error_codes.entry(err_code.clone()).or_default().has_explanation = true;
// Now we extract the tests from the markdown file!
let md_file_name = match s.split_once("include_str!(\"") {
None => continue,
Some((_, md)) => match md.split_once("\")") {
None => continue,
Some((file_name, _)) => file_name,
},
};
let path = some_or_continue!(path.parent())
.join(md_file_name)
.canonicalize()
.expect("failed to canonicalize error explanation file path");
match read_to_string(&path) {
Ok(content) => {
let has_test = check_if_error_code_is_test_in_explanation(&content, &err_code);
if !has_test && !IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
errors.push(format!(
"`{}` doesn't use its own error code in compile_fail example",
path.display(),
));
} else if has_test && IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
errors.push(format!(
"`{}` has a compile_fail example with its own error code, it shouldn't \
be listed in IGNORE_EXPLANATION_CHECK!",
path.display(),
));
}
if check_error_code_explanation(&content, error_codes, err_code) {
errors.push(format!(
"`{}` uses invalid tag `compile-fail` instead of `compile_fail`",
path.display(),
));
}
}
Err(e) => {
eprintln!("Couldn't read `{}`: {}", path.display(), e);
}
}
} else if reached_no_explanation && s.starts_with('E') {
let err_code = match s.split_once(',') {
None => s,
Some((err_code, _)) => err_code,
}
.to_string();
if !error_codes.contains_key(&err_code) {
// this check should *never* fail!
error_codes.insert(err_code, ErrorCodeStatus::default());
}
} else if s == ";" {
reached_no_explanation = true;
}
}
}
fn extract_error_codes_from_tests(f: &str, error_codes: &mut HashMap<String, ErrorCodeStatus>) {
for line in f.lines() {
let s = line.trim();
if s.starts_with("error[E") || s.starts_with("warning[E") {
let err_code = match s.split_once(']') {
None => continue,
Some((err_code, _)) => match err_code.split_once('[') {
None => continue,
Some((_, err_code)) => err_code,
},
};
error_codes.entry(err_code.to_owned()).or_default().has_test = true;
}
}
}
fn extract_error_codes_from_source(
f: &str,
error_codes: &mut HashMap<String, ErrorCodeStatus>,
regex: &Regex,
) {
for line in f.lines() {
if line.trim_start().starts_with("//") {
continue;
}
for cap in regex.captures_iter(line) {
if let Some(error_code) = cap.get(1) {
error_codes.entry(error_code.as_str().to_owned()).or_default().is_used = true;
}
}
}
}
pub fn check(paths: &[&Path], bad: &mut bool) {
let mut errors = Vec::new();
let mut found_explanations = 0;
let mut found_tests = 0;
let mut error_codes: HashMap<String, ErrorCodeStatus> = HashMap::new();
let mut explanations: HashSet<String> = HashSet::new();
// We want error codes which match the following cases:
//
// * foo(a, E0111, a)
// * foo(a, E0111)
// * foo(E0111, a)
// * #[error = "E0111"]
let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();
println!("Checking which error codes lack tests...");
for path in paths {
walk(path, &mut filter_dirs, &mut |entry, contents| {
let file_name = entry.file_name();
let entry_path = entry.path();
if file_name == "error_codes.rs" {
extract_error_codes(contents, &mut error_codes, entry.path(), &mut errors);
found_explanations += 1;
} else if entry_path.extension() == Some(OsStr::new("stderr")) {
extract_error_codes_from_tests(contents, &mut error_codes);
found_tests += 1;
} else if entry_path.extension() == Some(OsStr::new("rs")) {
let path = entry.path().to_string_lossy();
if PATHS_TO_IGNORE_FOR_EXTRACTION.iter().all(|c| !path.contains(c)) {
extract_error_codes_from_source(contents, &mut error_codes, &regex);
}
} else if entry_path
.parent()
.and_then(|p| p.file_name())
.map(|p| p == "error_codes")
.unwrap_or(false)
&& entry_path.extension() == Some(OsStr::new("md"))
{
explanations.insert(file_name.to_str().unwrap().replace(".md", ""));
}
});
}
if found_explanations == 0 {
eprintln!("No error code explanation was tested!");
*bad = true;
}
if found_tests == 0 {
eprintln!("No error code was found in compilation errors!");
*bad = true;
}
if explanations.is_empty() {
eprintln!("No error code explanation was found!");
*bad = true;
}
if errors.is_empty() {
println!("Found {} error codes", error_codes.len());
for (err_code, error_status) in &error_codes {
if !error_status.has_test && !EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
errors.push(format!("Error code {err_code} needs to have at least one UI test!"));
} else if error_status.has_test && EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
errors.push(format!(
"Error code {} has a UI test, it shouldn't be listed into EXEMPTED_FROM_TEST!",
err_code
));
}
if !error_status.is_used && !error_status.has_explanation {
errors.push(format!(
"Error code {} isn't used and doesn't have an error explanation, it should be \
commented in error_codes.rs file",
err_code
));
}
}
}
if errors.is_empty() {
// Checking if local constants need to be cleaned.
for err_code in EXEMPTED_FROM_TEST {
match error_codes.get(err_code.to_owned()) {
Some(status) => {
if status.has_test {
errors.push(format!(
"{} error code has a test and therefore should be \
removed from the `EXEMPTED_FROM_TEST` constant",
err_code
));
}
}
None => errors.push(format!(
"{} error code isn't used anymore and therefore should be removed \
from `EXEMPTED_FROM_TEST` constant",
err_code
)),
}
}
}
if errors.is_empty() {
for explanation in explanations {
if !error_codes.contains_key(&explanation) {
errors.push(format!(
"{} error code explanation should be listed in `error_codes.rs`",
explanation
));
}
}
}
errors.sort();
for err in &errors {
eprintln!("{err}");
}
println!("Found {} error(s) in error codes", errors.len());
if !errors.is_empty() {
*bad = true;
}
println!("Done!");
}