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(root_path: &Path, search_paths: &[&Path], verbose: bool, bad: &mut bool) {
47 let mut errors = Vec::new();
48
49 let error_codes = extract_error_codes(root_path, &mut errors);
51 if verbose {
52 println!("Found {} error codes", error_codes.len());
53 println!("Highest error code: `{}`", error_codes.iter().max().unwrap());
54 }
55
56 let no_longer_emitted = check_error_codes_docs(root_path, &error_codes, &mut errors, verbose);
58
59 check_error_codes_tests(root_path, &error_codes, &mut errors, verbose, &no_longer_emitted);
61
62 check_error_codes_used(search_paths, &error_codes, &mut errors, &no_longer_emitted, verbose);
64
65 for error in errors {
67 tidy_error!(bad, "{}", error);
68 }
69}
70
71fn extract_error_codes(root_path: &Path, errors: &mut Vec<String>) -> Vec<String> {
73 let path = root_path.join(Path::new(ERROR_CODES_PATH));
74 let file =
75 fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read `{path:?}`: {e}"));
76 let path = path.display();
77
78 let mut error_codes = Vec::new();
79
80 for (line_index, line) in file.lines().enumerate() {
81 let line_index = line_index + 1;
82 let line = line.trim();
83
84 if line.starts_with('E') {
85 let split_line = line.split_once(':');
86
87 let Some(split_line) = split_line else {
90 errors.push(format!(
91 "{path}:{line_index}: Expected a line with the format `Eabcd: abcd, \
92 but got \"{}\" without a `:` delimiter",
93 line,
94 ));
95 continue;
96 };
97
98 let err_code = split_line.0.to_owned();
99
100 if error_codes.contains(&err_code) {
102 errors.push(format!(
103 "{path}:{line_index}: Found duplicate error code: `{}`",
104 err_code
105 ));
106 continue;
107 }
108
109 let mut chars = err_code.chars();
110 assert_eq!(chars.next(), Some('E'));
111 let error_num_as_str = chars.as_str();
112
113 let rest = split_line.1.split_once(',');
115 let Some(rest) = rest else {
116 errors.push(format!(
117 "{path}:{line_index}: Expected a line with the format `Eabcd: abcd, \
118 but got \"{}\" without a `,` delimiter",
119 line,
120 ));
121 continue;
122 };
123 if error_num_as_str != rest.0.trim() {
124 errors.push(format!(
125 "{path}:{line_index}: `{}:` should be followed by `{},` but instead found `{}` in \
126 `compiler/rustc_error_codes/src/lib.rs`",
127 err_code,
128 error_num_as_str,
129 split_line.1,
130 ));
131 continue;
132 }
133 if !rest.1.trim().is_empty() && !rest.1.trim().starts_with("//") {
134 errors.push(format!("{path}:{line_index}: should only have one error per line"));
135 continue;
136 }
137
138 error_codes.push(err_code);
139 }
140 }
141
142 error_codes
143}
144
145fn check_error_codes_docs(
147 root_path: &Path,
148 error_codes: &[String],
149 errors: &mut Vec<String>,
150 verbose: bool,
151) -> Vec<String> {
152 let docs_path = root_path.join(Path::new(ERROR_DOCS_PATH));
153
154 let mut no_longer_emitted_codes = Vec::new();
155
156 walk(&docs_path, |_, _| false, &mut |entry, contents| {
157 let path = entry.path();
158
159 if path.extension() != Some(OsStr::new("md")) {
161 errors.push(format!(
162 "Found unexpected non-markdown file in error code docs directory: {}",
163 path.display()
164 ));
165 return;
166 }
167
168 let filename = path.file_name().unwrap().to_str().unwrap().split_once('.');
170 let err_code = filename.unwrap().0; if error_codes.iter().all(|e| e != err_code) {
173 errors.push(format!(
174 "Found valid file `{}` in error code docs directory without corresponding \
175 entry in `rustc_error_codes/src/lib.rs`",
176 path.display()
177 ));
178 return;
179 }
180
181 let (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted) =
182 check_explanation_has_doctest(&contents, &err_code);
183
184 if emit_ignore_warning {
185 verbose_print!(
186 verbose,
187 "warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
188 `IGNORE_DOCTEST_CHECK` constant instead."
189 );
190 }
191
192 if no_longer_emitted {
193 no_longer_emitted_codes.push(err_code.to_owned());
194 }
195
196 if !found_code_example {
197 verbose_print!(
198 verbose,
199 "warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
200 (even if untested)."
201 );
202 return;
203 }
204
205 let test_ignored = IGNORE_DOCTEST_CHECK.contains(&&err_code);
206
207 if !found_proper_doctest && !test_ignored {
209 errors.push(format!(
210 "`{}` doesn't use its own error code in compile_fail example",
211 path.display(),
212 ));
213 } else if found_proper_doctest && test_ignored {
214 errors.push(format!(
215 "`{}` has a compile_fail doctest with its own error code, it shouldn't \
216 be listed in `IGNORE_DOCTEST_CHECK`",
217 path.display(),
218 ));
219 }
220 });
221
222 no_longer_emitted_codes
223}
224
225fn check_explanation_has_doctest(explanation: &str, err_code: &str) -> (bool, bool, bool, bool) {
229 let mut found_code_example = false;
230 let mut found_proper_doctest = false;
231
232 let mut emit_ignore_warning = false;
233 let mut no_longer_emitted = false;
234
235 for line in explanation.lines() {
236 let line = line.trim();
237
238 if line.starts_with("```") {
239 found_code_example = true;
240
241 if line.contains("compile_fail") && line.contains(err_code) {
243 found_proper_doctest = true;
244 }
245
246 if line.contains("ignore") {
247 emit_ignore_warning = true;
248 found_proper_doctest = true;
249 }
250 } else if line
251 .starts_with("#### Note: this error code is no longer emitted by the compiler")
252 {
253 no_longer_emitted = true;
254 found_code_example = true;
255 found_proper_doctest = true;
256 }
257 }
258
259 (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted)
260}
261
262fn check_error_codes_tests(
264 root_path: &Path,
265 error_codes: &[String],
266 errors: &mut Vec<String>,
267 verbose: bool,
268 no_longer_emitted: &[String],
269) {
270 let tests_path = root_path.join(Path::new(ERROR_TESTS_PATH));
271
272 for code in error_codes {
273 let test_path = tests_path.join(format!("{}.stderr", code));
274
275 if !test_path.exists() && !IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
276 verbose_print!(
277 verbose,
278 "warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
279 );
280 continue;
281 }
282 if IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
283 if test_path.exists() {
284 errors.push(format!(
285 "Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
286 ));
287 }
288 continue;
289 }
290
291 let file = match fs::read_to_string(&test_path) {
292 Ok(file) => file,
293 Err(err) => {
294 verbose_print!(
295 verbose,
296 "warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
297 test_path.display()
298 );
299 continue;
300 }
301 };
302
303 if no_longer_emitted.contains(code) {
304 continue;
306 }
307
308 let mut found_code = false;
309
310 for line in file.lines() {
311 let s = line.trim();
312 if s.starts_with("error[E") && &s[6..11] == code {
314 found_code = true;
315 break;
316 };
317 }
318
319 if !found_code {
320 verbose_print!(
321 verbose,
322 "warning: Error code `{code}` has a UI test file, but doesn't contain its own error code!"
323 );
324 }
325 }
326}
327
328fn check_error_codes_used(
330 search_paths: &[&Path],
331 error_codes: &[String],
332 errors: &mut Vec<String>,
333 no_longer_emitted: &[String],
334 verbose: bool,
335) {
336 let regex = Regex::new(r#"\bE\d{4}\b"#).unwrap();
338
339 let mut found_codes = Vec::new();
340
341 walk_many(search_paths, |path, _is_dir| filter_dirs(path), &mut |entry, contents| {
342 let path = entry.path();
343
344 if path.extension() != Some(OsStr::new("rs")) {
346 return;
347 }
348
349 for line in contents.lines() {
350 if line.trim_start().starts_with("//") {
352 continue;
353 }
354
355 for cap in regex.captures_iter(line) {
356 if let Some(error_code) = cap.get(0) {
357 let error_code = error_code.as_str().to_owned();
358
359 if !error_codes.contains(&error_code) {
360 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));
362 continue;
363 }
364
365 found_codes.push(error_code);
367 }
368 }
369 }
370 });
371
372 for code in error_codes {
373 if !found_codes.contains(code) && !no_longer_emitted.contains(code) {
374 errors.push(format!(
375 "Error code `{code}` exists, but is not emitted by the compiler!\n\
376 Please mark the code as no longer emitted by adding the following note to the top of the `EXXXX.md` file:\n\
377 `#### Note: this error code is no longer emitted by the compiler`\n\
378 Also, do not forget to mark doctests that no longer apply as `ignore (error is no longer emitted)`."
379 ));
380 }
381
382 if found_codes.contains(code) && no_longer_emitted.contains(code) {
383 verbose_print!(
384 verbose,
385 "warning: Error code `{code}` is used when it's marked as \"no longer emitted\""
386 );
387 }
388 }
389}