]> git.proxmox.com Git - rustc.git/blame - src/tools/tidy/src/error_codes_check.rs
New upstream version 1.61.0+dfsg1
[rustc.git] / src / tools / tidy / src / error_codes_check.rs
CommitLineData
e74abb32
XL
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.
3
5099ac24 4use std::collections::{HashMap, HashSet};
e74abb32 5use std::ffi::OsStr;
60c5eb7d 6use std::fs::read_to_string;
e74abb32
XL
7use std::path::Path;
8
136023e0
XL
9use regex::Regex;
10
e74abb32 11// A few of those error codes can't be tested but all the others can and *should* be tested!
f035d41b 12const EXEMPTED_FROM_TEST: &[&str] = &[
ee023bcb
FG
13 "E0279", "E0313", "E0377", "E0461", "E0462", "E0465", "E0476", "E0514", "E0519", "E0523",
14 "E0554", "E0640", "E0717", "E0729",
e74abb32
XL
15];
16
f9f354fc 17// Some error codes don't have any tests apparently...
c295e0f8 18const IGNORE_EXPLANATION_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0729"];
f9f354fc 19
136023e0
XL
20// If the file path contains any of these, we don't want to try to extract error codes from it.
21//
22// We need to declare each path in the windows version (with backslash).
23const PATHS_TO_IGNORE_FOR_EXTRACTION: &[&str] =
24 &["src/test/", "src\\test\\", "src/doc/", "src\\doc\\", "src/tools/", "src\\tools\\"];
25
26#[derive(Default, Debug)]
27struct ErrorCodeStatus {
28 has_test: bool,
29 has_explanation: bool,
30 is_used: bool,
31}
32
60c5eb7d
XL
33fn check_error_code_explanation(
34 f: &str,
136023e0 35 error_codes: &mut HashMap<String, ErrorCodeStatus>,
60c5eb7d 36 err_code: String,
f9f354fc
XL
37) -> bool {
38 let mut invalid_compile_fail_format = false;
39 let mut found_error_code = false;
40
60c5eb7d
XL
41 for line in f.lines() {
42 let s = line.trim();
f9f354fc
XL
43 if s.starts_with("```") {
44 if s.contains("compile_fail") && s.contains('E') {
45 if !found_error_code {
136023e0 46 error_codes.get_mut(&err_code).map(|x| x.has_test = true);
f9f354fc
XL
47 found_error_code = true;
48 }
49 } else if s.contains("compile-fail") {
50 invalid_compile_fail_format = true;
51 }
60c5eb7d 52 } else if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
f9f354fc 53 if !found_error_code {
136023e0 54 error_codes.get_mut(&err_code).map(|x| x.has_test = true);
f9f354fc
XL
55 found_error_code = true;
56 }
60c5eb7d
XL
57 }
58 }
f9f354fc
XL
59 invalid_compile_fail_format
60}
61
3dfed10e 62fn check_if_error_code_is_test_in_explanation(f: &str, err_code: &str) -> bool {
cdc7bbd5
XL
63 let mut ignore_found = false;
64
f9f354fc
XL
65 for line in f.lines() {
66 let s = line.trim();
67 if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
68 return true;
69 }
70 if s.starts_with("```") {
71 if s.contains("compile_fail") && s.contains(err_code) {
72 return true;
cdc7bbd5 73 } else if s.contains("ignore") {
f9f354fc 74 // It's very likely that we can't actually make it fail compilation...
cdc7bbd5 75 ignore_found = true;
f9f354fc
XL
76 }
77 }
78 }
cdc7bbd5 79 ignore_found
60c5eb7d
XL
80}
81
82macro_rules! some_or_continue {
dfeec247 83 ($e:expr) => {
60c5eb7d
XL
84 match $e {
85 Some(e) => e,
86 None => continue,
87 }
dfeec247 88 };
60c5eb7d
XL
89}
90
f9f354fc
XL
91fn extract_error_codes(
92 f: &str,
136023e0 93 error_codes: &mut HashMap<String, ErrorCodeStatus>,
f9f354fc
XL
94 path: &Path,
95 errors: &mut Vec<String>,
96) {
e74abb32 97 let mut reached_no_explanation = false;
e74abb32
XL
98
99 for line in f.lines() {
100 let s = line.trim();
60c5eb7d 101 if !reached_no_explanation && s.starts_with('E') && s.contains("include_str!(\"") {
fc512014
XL
102 let err_code = s
103 .split_once(':')
104 .expect(
105 format!(
136023e0
XL
106 "Expected a line with the format `E0xxx: include_str!(\"..\")`, but got {} \
107 without a `:` delimiter",
fc512014 108 s,
136023e0
XL
109 )
110 .as_str(),
fc512014
XL
111 )
112 .0
113 .to_owned();
136023e0
XL
114 error_codes.entry(err_code.clone()).or_default().has_explanation = true;
115
fc512014
XL
116 // Now we extract the tests from the markdown file!
117 let md_file_name = match s.split_once("include_str!(\"") {
118 None => continue,
119 Some((_, md)) => match md.split_once("\")") {
120 None => continue,
121 Some((file_name, _)) => file_name,
122 },
123 };
124 let path = some_or_continue!(path.parent())
125 .join(md_file_name)
126 .canonicalize()
127 .expect("failed to canonicalize error explanation file path");
128 match read_to_string(&path) {
129 Ok(content) => {
17df50a5
XL
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()) {
fc512014
XL
132 errors.push(format!(
133 "`{}` doesn't use its own error code in compile_fail example",
134 path.display(),
135 ));
17df50a5
XL
136 } else if has_test && IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
137 errors.push(format!(
138 "`{}` has a compile_fail example with its own error code, it shouldn't \
139 be listed in IGNORE_EXPLANATION_CHECK!",
140 path.display(),
141 ));
60c5eb7d 142 }
fc512014
XL
143 if check_error_code_explanation(&content, error_codes, err_code) {
144 errors.push(format!(
145 "`{}` uses invalid tag `compile-fail` instead of `compile_fail`",
146 path.display(),
147 ));
60c5eb7d 148 }
e74abb32 149 }
fc512014
XL
150 Err(e) => {
151 eprintln!("Couldn't read `{}`: {}", path.display(), e);
152 }
e74abb32 153 }
e74abb32 154 } else if reached_no_explanation && s.starts_with('E') {
fc512014
XL
155 let err_code = match s.split_once(',') {
156 None => s,
157 Some((err_code, _)) => err_code,
158 }
159 .to_string();
160 if !error_codes.contains_key(&err_code) {
161 // this check should *never* fail!
136023e0 162 error_codes.insert(err_code, ErrorCodeStatus::default());
e74abb32 163 }
60c5eb7d
XL
164 } else if s == ";" {
165 reached_no_explanation = true;
e74abb32
XL
166 }
167 }
168}
169
136023e0 170fn extract_error_codes_from_tests(f: &str, error_codes: &mut HashMap<String, ErrorCodeStatus>) {
e74abb32
XL
171 for line in f.lines() {
172 let s = line.trim();
173 if s.starts_with("error[E") || s.starts_with("warning[E") {
fc512014
XL
174 let err_code = match s.split_once(']') {
175 None => continue,
176 Some((err_code, _)) => match err_code.split_once('[') {
177 None => continue,
178 Some((_, err_code)) => err_code,
179 },
180 };
136023e0
XL
181 error_codes.entry(err_code.to_owned()).or_default().has_test = true;
182 }
183 }
184}
185
186fn extract_error_codes_from_source(
187 f: &str,
188 error_codes: &mut HashMap<String, ErrorCodeStatus>,
189 regex: &Regex,
190) {
191 for line in f.lines() {
192 if line.trim_start().starts_with("//") {
193 continue;
194 }
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;
198 }
e74abb32
XL
199 }
200 }
201}
202
cdc7bbd5 203pub fn check(paths: &[&Path], bad: &mut bool) {
f9f354fc 204 let mut errors = Vec::new();
cdc7bbd5
XL
205 let mut found_explanations = 0;
206 let mut found_tests = 0;
136023e0 207 let mut error_codes: HashMap<String, ErrorCodeStatus> = HashMap::new();
5099ac24 208 let mut explanations: HashSet<String> = HashSet::new();
136023e0
XL
209 // We want error codes which match the following cases:
210 //
211 // * foo(a, E0111, a)
212 // * foo(a, E0111)
213 // * foo(E0111, a)
214 // * #[error = "E0111"]
215 let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();
216
e74abb32 217 println!("Checking which error codes lack tests...");
136023e0 218
cdc7bbd5
XL
219 for path in paths {
220 super::walk(path, &mut |path| super::filter_dirs(path), &mut |entry, contents| {
221 let file_name = entry.file_name();
5099ac24
FG
222 let entry_path = entry.path();
223
cdc7bbd5
XL
224 if file_name == "error_codes.rs" {
225 extract_error_codes(contents, &mut error_codes, entry.path(), &mut errors);
226 found_explanations += 1;
5099ac24 227 } else if entry_path.extension() == Some(OsStr::new("stderr")) {
cdc7bbd5
XL
228 extract_error_codes_from_tests(contents, &mut error_codes);
229 found_tests += 1;
5099ac24 230 } else if entry_path.extension() == Some(OsStr::new("rs")) {
136023e0
XL
231 let path = entry.path().to_string_lossy();
232 if PATHS_TO_IGNORE_FOR_EXTRACTION.iter().all(|c| !path.contains(c)) {
233 extract_error_codes_from_source(contents, &mut error_codes, &regex);
234 }
5099ac24
FG
235 } else if entry_path
236 .parent()
237 .and_then(|p| p.file_name())
238 .map(|p| p == "error_codes")
239 .unwrap_or(false)
240 && entry_path.extension() == Some(OsStr::new("md"))
241 {
242 explanations.insert(file_name.to_str().unwrap().replace(".md", ""));
cdc7bbd5
XL
243 }
244 });
245 }
246 if found_explanations == 0 {
247 eprintln!("No error code explanation was tested!");
248 *bad = true;
249 }
250 if found_tests == 0 {
251 eprintln!("No error code was found in compilation errors!");
252 *bad = true;
253 }
5099ac24
FG
254 if explanations.is_empty() {
255 eprintln!("No error code explanation was found!");
256 *bad = true;
257 }
f9f354fc
XL
258 if errors.is_empty() {
259 println!("Found {} error codes", error_codes.len());
e74abb32 260
136023e0
XL
261 for (err_code, error_status) in &error_codes {
262 if !error_status.has_test && !EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
ee023bcb 263 errors.push(format!("Error code {err_code} needs to have at least one UI test!"));
136023e0 264 } else if error_status.has_test && EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
17df50a5
XL
265 errors.push(format!(
266 "Error code {} has a UI test, it shouldn't be listed into EXEMPTED_FROM_TEST!",
267 err_code
268 ));
f9f354fc 269 }
136023e0
XL
270 if !error_status.is_used && !error_status.has_explanation {
271 errors.push(format!(
272 "Error code {} isn't used and doesn't have an error explanation, it should be \
273 commented in error_codes.rs file",
274 err_code
275 ));
276 }
277 }
278 }
279 if errors.is_empty() {
280 // Checking if local constants need to be cleaned.
281 for err_code in EXEMPTED_FROM_TEST {
282 match error_codes.get(err_code.to_owned()) {
283 Some(status) => {
284 if status.has_test {
285 errors.push(format!(
286 "{} error code has a test and therefore should be \
287 removed from the `EXEMPTED_FROM_TEST` constant",
288 err_code
289 ));
290 }
291 }
292 None => errors.push(format!(
293 "{} error code isn't used anymore and therefore should be removed \
294 from `EXEMPTED_FROM_TEST` constant",
295 err_code
296 )),
297 }
e74abb32
XL
298 }
299 }
5099ac24
FG
300 if errors.is_empty() {
301 for explanation in explanations {
302 if !error_codes.contains_key(&explanation) {
303 errors.push(format!(
304 "{} error code explanation should be listed in `error_codes.rs`",
305 explanation
306 ));
307 }
308 }
309 }
e74abb32
XL
310 errors.sort();
311 for err in &errors {
ee023bcb 312 eprintln!("{err}");
e74abb32 313 }
5099ac24 314 println!("Found {} error(s) in error codes", errors.len());
e74abb32
XL
315 if !errors.is_empty() {
316 *bad = true;
317 }
318 println!("Done!");
319}