]>
Commit | Line | Data |
---|---|---|
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 | ||
4 | use std::collections::HashMap; | |
5 | use std::ffi::OsStr; | |
60c5eb7d | 6 | use std::fs::read_to_string; |
e74abb32 XL |
7 | use std::path::Path; |
8 | ||
136023e0 XL |
9 | use 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 | 12 | const EXEMPTED_FROM_TEST: &[&str] = &[ |
136023e0 XL |
13 | "E0227", "E0279", "E0280", "E0313", "E0377", "E0461", "E0462", "E0464", "E0465", "E0476", |
14 | "E0482", "E0514", "E0519", "E0523", "E0554", "E0640", "E0717", "E0729", | |
e74abb32 XL |
15 | ]; |
16 | ||
f9f354fc | 17 | // Some error codes don't have any tests apparently... |
17df50a5 | 18 | const IGNORE_EXPLANATION_CHECK: &[&str] = &["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). | |
23 | const PATHS_TO_IGNORE_FOR_EXTRACTION: &[&str] = | |
24 | &["src/test/", "src\\test\\", "src/doc/", "src\\doc\\", "src/tools/", "src\\tools\\"]; | |
25 | ||
26 | #[derive(Default, Debug)] | |
27 | struct ErrorCodeStatus { | |
28 | has_test: bool, | |
29 | has_explanation: bool, | |
30 | is_used: bool, | |
31 | } | |
32 | ||
60c5eb7d XL |
33 | fn 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 | 62 | fn 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 | ||
82 | macro_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 |
91 | fn 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 | 170 | fn 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 | ||
186 | fn 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 | 203 | pub 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 XL |
207 | let mut error_codes: HashMap<String, ErrorCodeStatus> = HashMap::new(); |
208 | // We want error codes which match the following cases: | |
209 | // | |
210 | // * foo(a, E0111, a) | |
211 | // * foo(a, E0111) | |
212 | // * foo(E0111, a) | |
213 | // * #[error = "E0111"] | |
214 | let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap(); | |
215 | ||
e74abb32 | 216 | println!("Checking which error codes lack tests..."); |
136023e0 | 217 | |
cdc7bbd5 XL |
218 | for path in paths { |
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); | |
226 | found_tests += 1; | |
136023e0 XL |
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); | |
231 | } | |
cdc7bbd5 XL |
232 | } |
233 | }); | |
234 | } | |
235 | if found_explanations == 0 { | |
236 | eprintln!("No error code explanation was tested!"); | |
237 | *bad = true; | |
238 | } | |
239 | if found_tests == 0 { | |
240 | eprintln!("No error code was found in compilation errors!"); | |
241 | *bad = true; | |
242 | } | |
f9f354fc XL |
243 | if errors.is_empty() { |
244 | println!("Found {} error codes", error_codes.len()); | |
e74abb32 | 245 | |
136023e0 XL |
246 | for (err_code, error_status) in &error_codes { |
247 | if !error_status.has_test && !EXEMPTED_FROM_TEST.contains(&err_code.as_str()) { | |
f9f354fc | 248 | errors.push(format!("Error code {} needs to have at least one UI test!", err_code)); |
136023e0 | 249 | } else if error_status.has_test && EXEMPTED_FROM_TEST.contains(&err_code.as_str()) { |
17df50a5 XL |
250 | errors.push(format!( |
251 | "Error code {} has a UI test, it shouldn't be listed into EXEMPTED_FROM_TEST!", | |
252 | err_code | |
253 | )); | |
f9f354fc | 254 | } |
136023e0 XL |
255 | if !error_status.is_used && !error_status.has_explanation { |
256 | errors.push(format!( | |
257 | "Error code {} isn't used and doesn't have an error explanation, it should be \ | |
258 | commented in error_codes.rs file", | |
259 | err_code | |
260 | )); | |
261 | } | |
262 | } | |
263 | } | |
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()) { | |
268 | Some(status) => { | |
269 | if status.has_test { | |
270 | errors.push(format!( | |
271 | "{} error code has a test and therefore should be \ | |
272 | removed from the `EXEMPTED_FROM_TEST` constant", | |
273 | err_code | |
274 | )); | |
275 | } | |
276 | } | |
277 | None => errors.push(format!( | |
278 | "{} error code isn't used anymore and therefore should be removed \ | |
279 | from `EXEMPTED_FROM_TEST` constant", | |
280 | err_code | |
281 | )), | |
282 | } | |
e74abb32 XL |
283 | } |
284 | } | |
285 | errors.sort(); | |
286 | for err in &errors { | |
287 | eprintln!("{}", err); | |
288 | } | |
289 | println!("Found {} error codes with no tests", errors.len()); | |
290 | if !errors.is_empty() { | |
291 | *bad = true; | |
292 | } | |
293 | println!("Done!"); | |
294 | } |