]> git.proxmox.com Git - rustc.git/blob - src/tools/tidy/src/error_codes.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / src / tools / tidy / src / error_codes.rs
1 //! Tidy check to ensure error codes are properly documented and tested.
2 //!
3 //! Overview of check:
4 //!
5 //! 1. We create a list of error codes used by the compiler. Error codes are extracted from `compiler/rustc_error_codes/src/error_codes.rs`.
6 //!
7 //! 2. We check that the error code has a long-form explanation in `compiler/rustc_error_codes/src/error_codes/`.
8 //! - The explanation is expected to contain a `doctest` that fails with the correct error code. (`EXEMPT_FROM_DOCTEST` *currently* bypasses this check)
9 //! - Note that other stylistic conventions for markdown files are checked in the `style.rs` tidy check.
10 //!
11 //! 3. We check that the error code has a UI test in `tests/ui/error-codes/`.
12 //! - We ensure that there is both a `Exxxx.rs` file and a corresponding `Exxxx.stderr` file.
13 //! - We also ensure that the error code is used in the tests.
14 //! - *Currently*, it is possible to opt-out of this check with the `EXEMPTED_FROM_TEST` constant.
15 //!
16 //! 4. We check that the error code is actually emitted by the compiler.
17 //! - This is done by searching `compiler/` with a regex.
18
19 use std::{ffi::OsStr, fs, path::Path};
20
21 use regex::Regex;
22
23 use crate::walk::{filter_dirs, walk, walk_many};
24
25 const ERROR_CODES_PATH: &str = "compiler/rustc_error_codes/src/error_codes.rs";
26 const ERROR_DOCS_PATH: &str = "compiler/rustc_error_codes/src/error_codes/";
27 const ERROR_TESTS_PATH: &str = "tests/ui/error-codes/";
28
29 // Error codes that (for some reason) can't have a doctest in their explanation. Error codes are still expected to provide a code example, even if untested.
30 const IGNORE_DOCTEST_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0640", "E0717"];
31
32 // Error codes that don't yet have a UI test. This list will eventually be removed.
33 const IGNORE_UI_TEST_CHECK: &[&str] =
34 &["E0461", "E0465", "E0514", "E0554", "E0640", "E0717", "E0729"];
35
36 macro_rules! verbose_print {
37 ($verbose:expr, $($fmt:tt)*) => {
38 if $verbose {
39 println!("{}", format_args!($($fmt)*));
40 }
41 };
42 }
43
44 pub fn check(root_path: &Path, search_paths: &[&Path], verbose: bool, bad: &mut bool) {
45 let mut errors = Vec::new();
46
47 // Stage 1: create list
48 let error_codes = extract_error_codes(root_path, &mut errors);
49 if verbose {
50 println!("Found {} error codes", error_codes.len());
51 println!("Highest error code: `{}`", error_codes.iter().max().unwrap());
52 }
53
54 // Stage 2: check list has docs
55 let no_longer_emitted = check_error_codes_docs(root_path, &error_codes, &mut errors, verbose);
56
57 // Stage 3: check list has UI tests
58 check_error_codes_tests(root_path, &error_codes, &mut errors, verbose, &no_longer_emitted);
59
60 // Stage 4: check list is emitted by compiler
61 check_error_codes_used(search_paths, &error_codes, &mut errors, &no_longer_emitted, verbose);
62
63 // Print any errors.
64 for error in errors {
65 tidy_error!(bad, "{}", error);
66 }
67 }
68
69 /// Stage 1: Parses a list of error codes from `error_codes.rs`.
70 fn extract_error_codes(root_path: &Path, errors: &mut Vec<String>) -> Vec<String> {
71 let path = root_path.join(Path::new(ERROR_CODES_PATH));
72 let file =
73 fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read `{path:?}`: {e}"));
74
75 let mut error_codes = Vec::new();
76
77 for line in file.lines() {
78 let line = line.trim();
79
80 if line.starts_with('E') {
81 let split_line = line.split_once(':');
82
83 // Extract the error code from the line, emitting a fatal error if it is not in a correct format.
84 let err_code = if let Some(err_code) = split_line {
85 err_code.0.to_owned()
86 } else {
87 errors.push(format!(
88 "Expected a line with the format `Exxxx: include_str!(\"..\")`, but got \"{}\" \
89 without a `:` delimiter",
90 line,
91 ));
92 continue;
93 };
94
95 // If this is a duplicate of another error code, emit a fatal error.
96 if error_codes.contains(&err_code) {
97 errors.push(format!("Found duplicate error code: `{}`", err_code));
98 continue;
99 }
100
101 // Ensure that the line references the correct markdown file.
102 let expected_filename = format!(" include_str!(\"./error_codes/{}.md\"),", err_code);
103 if expected_filename != split_line.unwrap().1 {
104 errors.push(format!(
105 "Error code `{}` expected to reference docs with `{}` but instead found `{}` in \
106 `compiler/rustc_error_codes/src/error_codes.rs`",
107 err_code,
108 expected_filename,
109 split_line.unwrap().1,
110 ));
111 continue;
112 }
113
114 error_codes.push(err_code);
115 }
116 }
117
118 error_codes
119 }
120
121 /// Stage 2: Checks that long-form error code explanations exist and have doctests.
122 fn check_error_codes_docs(
123 root_path: &Path,
124 error_codes: &[String],
125 errors: &mut Vec<String>,
126 verbose: bool,
127 ) -> Vec<String> {
128 let docs_path = root_path.join(Path::new(ERROR_DOCS_PATH));
129
130 let mut no_longer_emitted_codes = Vec::new();
131
132 walk(&docs_path, |_, _| false, &mut |entry, contents| {
133 let path = entry.path();
134
135 // Error if the file isn't markdown.
136 if path.extension() != Some(OsStr::new("md")) {
137 errors.push(format!(
138 "Found unexpected non-markdown file in error code docs directory: {}",
139 path.display()
140 ));
141 return;
142 }
143
144 // Make sure that the file is referenced in `error_codes.rs`
145 let filename = path.file_name().unwrap().to_str().unwrap().split_once('.');
146 let err_code = filename.unwrap().0; // `unwrap` is ok because we know the filename is in the correct format.
147
148 if error_codes.iter().all(|e| e != err_code) {
149 errors.push(format!(
150 "Found valid file `{}` in error code docs directory without corresponding \
151 entry in `error_code.rs`",
152 path.display()
153 ));
154 return;
155 }
156
157 let (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted) =
158 check_explanation_has_doctest(&contents, &err_code);
159
160 if emit_ignore_warning {
161 verbose_print!(
162 verbose,
163 "warning: Error code `{err_code}` uses the ignore header. This should not be used, add the error code to the \
164 `IGNORE_DOCTEST_CHECK` constant instead."
165 );
166 }
167
168 if no_longer_emitted {
169 no_longer_emitted_codes.push(err_code.to_owned());
170 }
171
172 if !found_code_example {
173 verbose_print!(
174 verbose,
175 "warning: Error code `{err_code}` doesn't have a code example, all error codes are expected to have one \
176 (even if untested)."
177 );
178 return;
179 }
180
181 let test_ignored = IGNORE_DOCTEST_CHECK.contains(&&err_code);
182
183 // Check that the explanation has a doctest, and if it shouldn't, that it doesn't
184 if !found_proper_doctest && !test_ignored {
185 errors.push(format!(
186 "`{}` doesn't use its own error code in compile_fail example",
187 path.display(),
188 ));
189 } else if found_proper_doctest && test_ignored {
190 errors.push(format!(
191 "`{}` has a compile_fail doctest with its own error code, it shouldn't \
192 be listed in `IGNORE_DOCTEST_CHECK`",
193 path.display(),
194 ));
195 }
196 });
197
198 no_longer_emitted_codes
199 }
200
201 /// This function returns a tuple indicating whether the provided explanation:
202 /// a) has a code example, tested or not.
203 /// b) has a valid doctest
204 fn check_explanation_has_doctest(explanation: &str, err_code: &str) -> (bool, bool, bool, bool) {
205 let mut found_code_example = false;
206 let mut found_proper_doctest = false;
207
208 let mut emit_ignore_warning = false;
209 let mut no_longer_emitted = false;
210
211 for line in explanation.lines() {
212 let line = line.trim();
213
214 if line.starts_with("```") {
215 found_code_example = true;
216
217 // Check for the `rustdoc` doctest headers.
218 if line.contains("compile_fail") && line.contains(err_code) {
219 found_proper_doctest = true;
220 }
221
222 if line.contains("ignore") {
223 emit_ignore_warning = true;
224 found_proper_doctest = true;
225 }
226 } else if line
227 .starts_with("#### Note: this error code is no longer emitted by the compiler")
228 {
229 no_longer_emitted = true;
230 found_code_example = true;
231 found_proper_doctest = true;
232 }
233 }
234
235 (found_code_example, found_proper_doctest, emit_ignore_warning, no_longer_emitted)
236 }
237
238 // Stage 3: Checks that each error code has a UI test in the correct directory
239 fn check_error_codes_tests(
240 root_path: &Path,
241 error_codes: &[String],
242 errors: &mut Vec<String>,
243 verbose: bool,
244 no_longer_emitted: &[String],
245 ) {
246 let tests_path = root_path.join(Path::new(ERROR_TESTS_PATH));
247
248 for code in error_codes {
249 let test_path = tests_path.join(format!("{}.stderr", code));
250
251 if !test_path.exists() && !IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
252 verbose_print!(
253 verbose,
254 "warning: Error code `{code}` needs to have at least one UI test in the `tests/error-codes/` directory`!"
255 );
256 continue;
257 }
258 if IGNORE_UI_TEST_CHECK.contains(&code.as_str()) {
259 if test_path.exists() {
260 errors.push(format!(
261 "Error code `{code}` has a UI test in `tests/ui/error-codes/{code}.rs`, it shouldn't be listed in `EXEMPTED_FROM_TEST`!"
262 ));
263 }
264 continue;
265 }
266
267 let file = match fs::read_to_string(&test_path) {
268 Ok(file) => file,
269 Err(err) => {
270 verbose_print!(
271 verbose,
272 "warning: Failed to read UI test file (`{}`) for `{code}` but the file exists. The test is assumed to work:\n{err}",
273 test_path.display()
274 );
275 continue;
276 }
277 };
278
279 if no_longer_emitted.contains(code) {
280 // UI tests *can't* contain error codes that are no longer emitted.
281 continue;
282 }
283
284 let mut found_code = false;
285
286 for line in file.lines() {
287 let s = line.trim();
288 // Assuming the line starts with `error[E`, we can substring the error code out.
289 if s.starts_with("error[E") {
290 if &s[6..11] == code {
291 found_code = true;
292 break;
293 }
294 };
295 }
296
297 if !found_code {
298 verbose_print!(
299 verbose,
300 "warning: Error code {code}`` has a UI test file, but doesn't contain its own error code!"
301 );
302 }
303 }
304 }
305
306 /// Stage 4: Search `compiler/` and ensure that every error code is actually used by the compiler and that no undocumented error codes exist.
307 fn check_error_codes_used(
308 search_paths: &[&Path],
309 error_codes: &[String],
310 errors: &mut Vec<String>,
311 no_longer_emitted: &[String],
312 verbose: bool,
313 ) {
314 // We want error codes which match the following cases:
315 //
316 // * foo(a, E0111, a)
317 // * foo(a, E0111)
318 // * foo(E0111, a)
319 // * #[error = "E0111"]
320 let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();
321
322 let mut found_codes = Vec::new();
323
324 walk_many(search_paths, |path, _is_dir| filter_dirs(path), &mut |entry, contents| {
325 let path = entry.path();
326
327 // Return early if we aren't looking at a source file.
328 if path.extension() != Some(OsStr::new("rs")) {
329 return;
330 }
331
332 for line in contents.lines() {
333 // We want to avoid parsing error codes in comments.
334 if line.trim_start().starts_with("//") {
335 continue;
336 }
337
338 for cap in regex.captures_iter(line) {
339 if let Some(error_code) = cap.get(1) {
340 let error_code = error_code.as_str().to_owned();
341
342 if !error_codes.contains(&error_code) {
343 // This error code isn't properly defined, we must error.
344 errors.push(format!("Error code `{}` is used in the compiler but not defined and documented in `compiler/rustc_error_codes/src/error_codes.rs`.", error_code));
345 continue;
346 }
347
348 // This error code can now be marked as used.
349 found_codes.push(error_code);
350 }
351 }
352 }
353 });
354
355 for code in error_codes {
356 if !found_codes.contains(code) && !no_longer_emitted.contains(code) {
357 errors.push(format!(
358 "Error code `{code}` exists, but is not emitted by the compiler!\n\
359 Please mark the code as no longer emitted by adding the following note to the top of the `EXXXX.md` file:\n\
360 `#### Note: this error code is no longer emitted by the compiler`\n\
361 Also, do not forget to mark doctests that no longer apply as `ignore (error is no longer emitted)`."
362 ));
363 }
364
365 if found_codes.contains(code) && no_longer_emitted.contains(code) {
366 verbose_print!(
367 verbose,
368 "warning: Error code `{code}` is used when it's marked as \"no longer emitted\""
369 );
370 }
371 }
372 }