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