1 //! Checks that all error codes have at least one test to prevent having error
2 //! codes that are silently not thrown by the compiler anymore.
4 use std::collections::HashMap;
6 use std::fs::read_to_string;
11 // A few of those error codes can't be tested but all the others can and *should* be tested!
12 const EXEMPTED_FROM_TEST: &[&str] = &[
13 "E0279", "E0280", "E0313", "E0377", "E0461", "E0462", "E0465", "E0476", "E0514", "E0519",
14 "E0523", "E0554", "E0640", "E0717", "E0729",
17 // Some error codes don't have any tests apparently...
18 const IGNORE_EXPLANATION_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0729"];
20 // If the file path contains any of these, we don't want to try to extract error codes from it.
22 // We need to declare each path in the windows version (with backslash).
23 const PATHS_TO_IGNORE_FOR_EXTRACTION: &[&str] =
24 &["src/test/", "src\\test\\", "src/doc/", "src\\doc\\", "src/tools/", "src\\tools\\"];
26 #[derive(Default, Debug)]
27 struct ErrorCodeStatus {
29 has_explanation: bool,
33 fn check_error_code_explanation(
35 error_codes: &mut HashMap<String, ErrorCodeStatus>,
38 let mut invalid_compile_fail_format = false;
39 let mut found_error_code = false;
41 for line in f.lines() {
43 if s.starts_with("```") {
44 if s.contains("compile_fail") && s.contains('E') {
45 if !found_error_code {
46 error_codes.get_mut(&err_code).map(|x| x.has_test = true);
47 found_error_code = true;
49 } else if s.contains("compile-fail") {
50 invalid_compile_fail_format = true;
52 } else if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
53 if !found_error_code {
54 error_codes.get_mut(&err_code).map(|x| x.has_test = true);
55 found_error_code = true;
59 invalid_compile_fail_format
62 fn check_if_error_code_is_test_in_explanation(f: &str, err_code: &str) -> bool {
63 let mut ignore_found = false;
65 for line in f.lines() {
67 if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
70 if s.starts_with("```") {
71 if s.contains("compile_fail") && s.contains(err_code) {
73 } else if s.contains("ignore") {
74 // It's very likely that we can't actually make it fail compilation...
82 macro_rules! some_or_continue {
91 fn extract_error_codes(
93 error_codes: &mut HashMap<String, ErrorCodeStatus>,
95 errors: &mut Vec<String>,
97 let mut reached_no_explanation = false;
99 for line in f.lines() {
101 if !reached_no_explanation && s.starts_with('E') && s.contains("include_str!(\"") {
106 "Expected a line with the format `E0xxx: include_str!(\"..\")`, but got {} \
107 without a `:` delimiter",
114 error_codes.entry(err_code.clone()).or_default().has_explanation = true;
116 // Now we extract the tests from the markdown file!
117 let md_file_name = match s.split_once("include_str!(\"") {
119 Some((_, md)) => match md.split_once("\")") {
121 Some((file_name, _)) => file_name,
124 let path = some_or_continue!(path.parent())
127 .expect("failed to canonicalize error explanation file path");
128 match read_to_string(&path) {
130 let has_test = check_if_error_code_is_test_in_explanation(&content, &err_code);
131 if !has_test && !IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
133 "`{}` doesn't use its own error code in compile_fail example",
136 } else if has_test && IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
138 "`{}` has a compile_fail example with its own error code, it shouldn't \
139 be listed in IGNORE_EXPLANATION_CHECK!",
143 if check_error_code_explanation(&content, error_codes, err_code) {
145 "`{}` uses invalid tag `compile-fail` instead of `compile_fail`",
151 eprintln!("Couldn't read `{}`: {}", path.display(), e);
154 } else if reached_no_explanation && s.starts_with('E') {
155 let err_code = match s.split_once(',') {
157 Some((err_code, _)) => err_code,
160 if !error_codes.contains_key(&err_code) {
161 // this check should *never* fail!
162 error_codes.insert(err_code, ErrorCodeStatus::default());
165 reached_no_explanation = true;
170 fn extract_error_codes_from_tests(f: &str, error_codes: &mut HashMap<String, ErrorCodeStatus>) {
171 for line in f.lines() {
173 if s.starts_with("error[E") || s.starts_with("warning[E") {
174 let err_code = match s.split_once(']') {
176 Some((err_code, _)) => match err_code.split_once('[') {
178 Some((_, err_code)) => err_code,
181 error_codes.entry(err_code.to_owned()).or_default().has_test = true;
186 fn extract_error_codes_from_source(
188 error_codes: &mut HashMap<String, ErrorCodeStatus>,
191 for line in f.lines() {
192 if line.trim_start().starts_with("//") {
195 for cap in regex.captures_iter(line) {
196 if let Some(error_code) = cap.get(1) {
197 error_codes.entry(error_code.as_str().to_owned()).or_default().is_used = true;
203 pub fn check(paths: &[&Path], bad: &mut bool) {
204 let mut errors = Vec::new();
205 let mut found_explanations = 0;
206 let mut found_tests = 0;
207 let mut error_codes: HashMap<String, ErrorCodeStatus> = HashMap::new();
208 // We want error codes which match the following cases:
210 // * foo(a, E0111, a)
213 // * #[error = "E0111"]
214 let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();
216 println!("Checking which error codes lack tests...");
219 super::walk(path, &mut |path| super::filter_dirs(path), &mut |entry, contents| {
220 let file_name = entry.file_name();
221 if file_name == "error_codes.rs" {
222 extract_error_codes(contents, &mut error_codes, entry.path(), &mut errors);
223 found_explanations += 1;
224 } else if entry.path().extension() == Some(OsStr::new("stderr")) {
225 extract_error_codes_from_tests(contents, &mut error_codes);
227 } else if entry.path().extension() == Some(OsStr::new("rs")) {
228 let path = entry.path().to_string_lossy();
229 if PATHS_TO_IGNORE_FOR_EXTRACTION.iter().all(|c| !path.contains(c)) {
230 extract_error_codes_from_source(contents, &mut error_codes, ®ex);
235 if found_explanations == 0 {
236 eprintln!("No error code explanation was tested!");
239 if found_tests == 0 {
240 eprintln!("No error code was found in compilation errors!");
243 if errors.is_empty() {
244 println!("Found {} error codes", error_codes.len());
246 for (err_code, error_status) in &error_codes {
247 if !error_status.has_test && !EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
248 errors.push(format!("Error code {} needs to have at least one UI test!", err_code));
249 } else if error_status.has_test && EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
251 "Error code {} has a UI test, it shouldn't be listed into EXEMPTED_FROM_TEST!",
255 if !error_status.is_used && !error_status.has_explanation {
257 "Error code {} isn't used and doesn't have an error explanation, it should be \
258 commented in error_codes.rs file",
264 if errors.is_empty() {
265 // Checking if local constants need to be cleaned.
266 for err_code in EXEMPTED_FROM_TEST {
267 match error_codes.get(err_code.to_owned()) {
271 "{} error code has a test and therefore should be \
272 removed from the `EXEMPTED_FROM_TEST` constant",
277 None => errors.push(format!(
278 "{} error code isn't used anymore and therefore should be removed \
279 from `EXEMPTED_FROM_TEST` constant",
287 eprintln!("{}", err);
289 println!("Found {} error codes with no tests", errors.len());
290 if !errors.is_empty() {