1use std::ffi::OsStr;
20use std::fs;
21use std::path::Path;
22
23use regex::Regex;
24
25use crate::walk::{filter_dirs, walk, walk_many};
26
27const ERROR_CODES_PATH: &str = "compiler/rustc_error_codes/src/lib.rs";
28const ERROR_DOCS_PATH: &str = "compiler/rustc_error_codes/src/error_codes/";
29const ERROR_TESTS_PATH: &str = "tests/ui/error-codes/";
30
31const IGNORE_DOCTEST_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0717"];
33
34const IGNORE_UI_TEST_CHECK: &[&str] =
36 &["E0461", "E0465", "E0514", "E0554", "E0640", "E0717", "E0729"];
37
38macro_rules! verbose_print {
39 ($verbose:expr, $($fmt:tt)*) => {
40 if $verbose {
41 println!("{}", format_args!($($fmt)*));
42 }
43 };
44}
45
46pub fn check(
47 root_path: &Path,
48 search_paths: &[&Path],
49 verbose: bool,
50 ci_info: &crate::CiInfo,
51 bad: &mut bool,
52) {
53 let mut errors = Vec::new();
54
55 check_removed_error_code_explanation(ci_info, bad);
57
58 let error_codes = extract_error_codes(root_path, &mut errors);
60 if verbose {
61 println!("Found {} error codes", error_codes.len());
62 println!("Highest error code: `{}`", error_codes.iter().max().unwrap());
63 }
64
65 let no_longer_emitted = check_error_codes_docs(root_path, &error_codes, &mut errors, verbose);
67
68 check_error_codes_tests(root_path, &error_codes, &mut errors, verbose, &no_longer_emitted);
70
71 check_error_codes_used(search_paths, &error_codes, &mut errors, &no_longer_emitted, verbose);
73
74 for error in errors {
76 tidy_error!(bad, "{}", error);
77 }
78}
79
80fn check_removed_error_code_explanation(ci_info: &crate::CiInfo, bad: &mut bool) {
81 let Some(base_commit) = &ci_info.base_commit else {
82 eprintln!("Skipping error code explanation removal check");
83 return;
84 };
85 let Some(diff) = crate::git_diff(base_commit, "--name-status") else {
86 *bad = true;
87 eprintln!("removed error code explanation tidy check: Failed to run git diff");
88 return;
89 };
90 if diff.lines().any(|line| {
91 line.starts_with('D') && line.contains("compiler/rustc_error_codes/src/error_codes/")
92 }) {
93 *bad = true;
94 eprintln!("tidy check error: Error code explanations should never be removed!");
95 eprintln!("Take a look at E0001 to see how to handle it.");
96 return;
97 }
98 println!("No error code explanation was removed!");
99}
100
101fn extract_error_codes(root_path: &Path, errors: &mut Vec<String>) -> Vec<String> {
103 let path = root_path.join(Path::new(ERROR_CODES_PATH));
104 let file =
105 fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read `{path:?}`: {e}"));
106 let path = path.display();
107
108 let mut error_codes = Vec::new();
109
110 for (line_index, line) in file.lines().enumerate() {
111 let line_index = line_index + 1;
112 let line = line.trim();
113
114 if line.starts_with('E') {
115 let split_line = line.split_once(':');
116
117 let Some(split_line) = split_line else {
120 errors.push(format!(
121 "{path}:{line_index}: Expected a line with the format `Eabcd: abcd, \
122 but got \"{}\" without a `:` delimiter",
123 line,
124 ));
125 continue;
126 };
127
128 let err_code = split_line.0.to_owned();
129
130 if error_codes.contains(&err_code) {
132 errors.push(format!(
133 "{path}:{line_index}: Found duplicate error code: `{}`",
134 err_code
135 ));
136 continue;
137 }
138
139 let mut chars = err_code.chars();
140 assert_eq!(chars.next(), Some('E'));
141 let error_num_as_str = chars.as_str();
142
143 let rest = split_line.1.split_once(',');
145 let Some(rest) = rest else {
146 errors.push(format!(
147 "{path}:{line_index}: Expected a line with the format `Eabcd: abcd, \
148 but got \"{}\" without a `,` delimiter",
149 line,
150 ));
151 continue;
152 };
153 if error_num_as_str != rest.0.trim() {
154 errors.push(format!(
155 "{path}:{line_index}: `{}:` should be followed by `{},` but instead found `{}` in \
156 `compiler/rustc_error_codes/src/lib.rs`",
157 err_code,
158 error_num_as_str,
159 split_line.1,
160 ));
161 continue;
162 }
163 if !rest.1.trim().is_empty() && !rest.1.trim().starts_with("//") {
164 errors.push(format!("{path}:{line_index}: should only have one error per line"));
165 continue;
166 }
167
168 error_codes.push(err_code);
169 }
170 }
171
172 error_codes
173}
174
175fn check_error_codes_docs(
177 root_path: &Path,
178 error_codes: &[String],
179 errors: &mut Vec<String>,
180 verbose: bool,
181) -> Vec<String> {
182 let docs_path = root_path.join(Path::new(ERROR_DOCS_PATH));
183
184 let mut no_longer_emitted_codes = Vec::new();
185
186 walk(&docs_path, |_, _| false, &mut |entry, contents| {
187 let path = entry.path();
188
189 if path.extension() != Some(OsStr::new("md")) {
191 errors.push(format!(
192 "Found unexpected non-markdown file in error code docs directory: {}",
193 path.display()
194 ));
195 return;
196 }
197
198 let filename = path.file_name().unwrap().to_str().unwrap().split_once('.');
200 let err_code = filename.unwrap().0; if error_codes.iter().all(|e| e != err_code) {
203 errors.push(format!(
204 "Found valid file `{}` in error code docs directory without corresponding \
205 entry in `rustc_error_codes/src/lib.rs`",
206 path.display()
207 ));
208 return;
209 }
210
211 let (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted) =
212 check_explanation_has_doctest(&contents, &err_code);
213
214 if emit_ignore_warning {
215 verbose_print!(
216 verbose,
217 "warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
218 `IGNORE_DOCTEST_CHECK` constant instead."
219 );
220 }
221
222 if no_longer_emitted {
223 no_longer_emitted_codes.push(err_code.to_owned());
224 }
225
226 if !found_code_example {
227 verbose_print!(
228 verbose,
229 "warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
230 (even if untested)."
231 );
232 return;
233 }
234
235 let test_ignored = IGNORE_DOCTEST_CHECK.contains(&&err_code);
236
237 if !found_proper_doctest && !test_ignored {
239 errors.push(format!(
240 "`{}` doesn't use its own error code in compile_fail example",
241 path.display(),
242 ));
243 } else if found_proper_doctest && test_ignored {
244 errors.push(format!(
245 "`{}` has a compile_fail doctest with its own error code, it shouldn't \
246 be listed in `IGNORE_DOCTEST_CHECK`",
247 path.display(),
248 ));
249 }
250 });
251
252 no_longer_emitted_codes
253}
254
255fn check_explanation_has_doctest(explanation: &str, err_code: &str) -> (bool, bool, bool, bool) {
259 let mut found_code_example = false;
260 let mut found_proper_doctest = false;
261
262 let mut emit_ignore_warning = false;
263 let mut no_longer_emitted = false;
264
265 for line in explanation.lines() {
266 let line = line.trim();
267
268 if line.starts_with("```") {
269 found_code_example = true;
270
271 if line.contains("compile_fail") && line.contains(err_code) {
273 found_proper_doctest = true;
274 }
275
276 if line.contains("ignore") {
277 emit_ignore_warning = true;
278 found_proper_doctest = true;
279 }
280 } else if line
281 .starts_with("#### Note: this error code is no longer emitted by the compiler")
282 {
283 no_longer_emitted = true;
284 found_code_example = true;
285 found_proper_doctest = true;
286 }
287 }
288
289 (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted)
290}
291
292fn check_error_codes_tests(
294 root_path: &Path,
295 error_codes: &[String],
296 errors: &mut Vec<String>,
297 verbose: bool,
298 no_longer_emitted: &[String],
299) {
300 let tests_path = root_path.join(Path::new(ERROR_TESTS_PATH));
301
302 for code in error_codes {
303 let test_path = tests_path.join(format!("{}.stderr", code));
304
305 if !test_path.exists() && !IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
306 verbose_print!(
307 verbose,
308 "warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
309 );
310 continue;
311 }
312 if IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
313 if test_path.exists() {
314 errors.push(format!(
315 "Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
316 ));
317 }
318 continue;
319 }
320
321 let file = match fs::read_to_string(&test_path) {
322 Ok(file) => file,
323 Err(err) => {
324 verbose_print!(
325 verbose,
326 "warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
327 test_path.display()
328 );
329 continue;
330 }
331 };
332
333 if no_longer_emitted.contains(code) {
334 continue;
336 }
337
338 let mut found_code = false;
339
340 for line in file.lines() {
341 let s = line.trim();
342 if s.starts_with("error[E") && &s[6..11] == code {
344 found_code = true;
345 break;
346 };
347 }
348
349 if !found_code {
350 verbose_print!(
351 verbose,
352 "warning: Error code `{code}` has a UI test file, but doesn't contain its own error code!"
353 );
354 }
355 }
356}
357
358fn check_error_codes_used(
360 search_paths: &[&Path],
361 error_codes: &[String],
362 errors: &mut Vec<String>,
363 no_longer_emitted: &[String],
364 verbose: bool,
365) {
366 let regex = Regex::new(r#"\bE\d{4}\b"#).unwrap();
368
369 let mut found_codes = Vec::new();
370
371 walk_many(search_paths, |path, _is_dir| filter_dirs(path), &mut |entry, contents| {
372 let path = entry.path();
373
374 if path.extension() != Some(OsStr::new("rs")) {
376 return;
377 }
378
379 for line in contents.lines() {
380 if line.trim_start().starts_with("//") {
382 continue;
383 }
384
385 for cap in regex.captures_iter(line) {
386 if let Some(error_code) = cap.get(0) {
387 let error_code = error_code.as_str().to_owned();
388
389 if !error_codes.contains(&error_code) {
390 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));
392 continue;
393 }
394
395 found_codes.push(error_code);
397 }
398 }
399 }
400 });
401
402 for code in error_codes {
403 if !found_codes.contains(code) && !no_longer_emitted.contains(code) {
404 errors.push(format!(
405 "Error code `{code}` exists, but is not emitted by the compiler!\n\
406 Please mark the code as no longer emitted by adding the following note to the top of the `EXXXX.md` file:\n\
407 `#### Note: this error code is no longer emitted by the compiler`\n\
408 Also, do not forget to mark doctests that no longer apply as `ignore (error is no longer emitted)`."
409 ));
410 }
411
412 if found_codes.contains(code) && no_longer_emitted.contains(code) {
413 verbose_print!(
414 verbose,
415 "warning: Error code `{code}` is used when it's marked as \"no longer emitted\""
416 );
417 }
418 }
419}