1use std::ffi::OsStr;
20use std::mem;
21use std::path::Path;
22use std::sync::LazyLock;
23
24use regex::RegexSetBuilder;
25use rustc_hash::FxHashMap;
26
27use crate::diagnostics::{CheckId, RunningCheck, TidyCtx};
28use crate::style::directive::{Directives, LineNumber, NamedDirective, match_ignore};
29use crate::walk::{filter_dirs, walk};
30
31mod directive;
32
33#[cfg(test)]
34mod tests;
35
36const ERROR_CODE_COLS: usize = 80;
39const COLS: usize = 100;
40const GOML_COLS: usize = 120;
41
42const LINES: usize = 3000;
43
44const UNEXPLAINED_IGNORE_DOCTEST_INFO: &str = r#"unexplained "```ignore" doctest; try one:
45
46* make the test actually pass, by adding necessary imports and declarations, or
47* use "```text", if the code is not Rust code, or
48* use "```compile_fail,Ennnn", if the code is expected to fail at compile time, or
49* use "```should_panic", if the code is expected to fail at run time, or
50* use "```no_run", if the code should type-check but not necessary linkable/runnable, or
51* explain it like "```ignore (cannot-test-this-because-xxxx)", if the annotation cannot be avoided.
52
53"#;
54
55const LLVM_UNREACHABLE_INFO: &str = r"\
56C++ code used llvm_unreachable, which triggers undefined behavior
57when executed when assertions are disabled.
58Use llvm::report_fatal_error for increased robustness.";
59
60const DOUBLE_SPACE_AFTER_DOT: &str = r"\
61Use a single space after dots in comments.";
62
63const ANNOTATIONS_TO_IGNORE: &[&str] = &[
64 "// @!has",
65 "// @has",
66 "// @matches",
67 "// CHECK",
68 "// EMIT_MIR",
69 "// compile-flags",
70 "//@ compile-flags",
71 "// error-pattern",
72 "//@ error-pattern",
73 "//@ gdb",
74 "//@ lldb",
75 "//@ cdb",
76 "//@ normalize-stderr",
77];
78
79fn generate_problems<'a>(
80 consts: &'a [u32],
81 letter_digit: &'a FxHashMap<char, char>,
82) -> impl Iterator<Item = u32> + 'a {
83 consts.iter().flat_map(move |const_value| {
84 let problem = letter_digit.iter().fold(format!("{const_value:X}"), |acc, (key, value)| {
85 acc.replace(&value.to_string(), &key.to_string())
86 });
87 let indexes: Vec<usize> = problem
88 .chars()
89 .enumerate()
90 .filter_map(|(index, c)| if letter_digit.contains_key(&c) { Some(index) } else { None })
91 .collect();
92 (0..1 << indexes.len()).map(move |i| {
93 u32::from_str_radix(
94 &problem
95 .chars()
96 .enumerate()
97 .map(|(index, c)| {
98 if let Some(pos) = indexes.iter().position(|&x| x == index) {
99 if (i >> pos) & 1 == 1 { letter_digit[&c] } else { c }
100 } else {
101 c
102 }
103 })
104 .collect::<String>(),
105 0x10,
106 )
107 .unwrap()
108 })
109 })
110}
111
112const ROOT_PROBLEMATIC_CONSTS: &[u32] = &[
114 184594741, 2880289470, 2881141438, 2965027518, 2976579765, 3203381950, 3405691582, 3405697037,
115 3735927486, 3735932941, 4027431614, 4276992702, 195934910, 252707358, 762133, 179681982,
116 173390526, 721077,
117];
118
119const LETTER_DIGIT: &[(char, char)] = &[('A', '4'), ('B', '8'), ('E', '3')];
120
121fn generate_problematic_strings(
123 consts: &[u32],
124 letter_digit: &FxHashMap<char, char>,
125) -> Vec<String> {
126 generate_problems(consts, letter_digit)
127 .flat_map(|v| vec![v.to_string(), format!("{:X}", v)])
128 .collect()
129}
130
131static PROBLEMATIC_CONSTS_STRINGS: LazyLock<Vec<String>> = LazyLock::new(|| {
132 generate_problematic_strings(ROOT_PROBLEMATIC_CONSTS, &LETTER_DIGIT.iter().cloned().collect())
133});
134
135fn contains_problematic_const(trimmed: &str) -> bool {
136 PROBLEMATIC_CONSTS_STRINGS.iter().any(|s| trimmed.to_uppercase().contains(s))
137}
138
139const INTERNAL_COMPILER_DOCS_LINE: &str = "#### This error code is internal to the compiler and will not be emitted with normal Rust code.";
140
141#[derive(Clone, Copy, PartialEq)]
143#[allow(non_camel_case_types)]
144enum LIUState {
145 EXP_COMMENT_START,
146 EXP_LINK_LABEL_OR_URL,
147 EXP_URL,
148 EXP_END,
149}
150
151fn line_is_url(is_error_code: bool, columns: usize, line: &str) -> bool {
158 if is_error_code {
160 return line.starts_with('[') && line.contains("]:") && line.contains("http");
161 }
162
163 use self::LIUState::*;
164 let mut state: LIUState = EXP_COMMENT_START;
165 let is_url = |w: &str| w.starts_with("http://") || w.starts_with("https://");
166
167 for tok in line.split_whitespace() {
168 match (state, tok) {
169 (EXP_COMMENT_START, "//") | (EXP_COMMENT_START, "///") | (EXP_COMMENT_START, "//!") => {
170 state = EXP_LINK_LABEL_OR_URL
171 }
172
173 (EXP_LINK_LABEL_OR_URL, w)
174 if w.len() >= 4 && w.starts_with('[') && w.ends_with("]:") =>
175 {
176 state = EXP_URL
177 }
178
179 (EXP_LINK_LABEL_OR_URL, w) if is_url(w) => state = EXP_END,
180
181 (EXP_URL, w) if is_url(w) || w.starts_with("../") => state = EXP_END,
182
183 (_, w) if w.len() > columns && is_url(w) => state = EXP_END,
184
185 (_, _) => {}
186 }
187 }
188
189 state == EXP_END
190}
191
192fn should_ignore(line: &str) -> bool {
195 static_regex!("\\s*//(\\[.*\\])?~.*").is_match(line)
199 || ANNOTATIONS_TO_IGNORE.iter().any(|a| line.contains(a))
200
201 || static_regex!("\\s*//@(\\[.*\\]) (compile-flags|normalize-stderr|error-pattern).*")
205 .is_match(line)
206 || static_regex!(
209 "\\s*//@ \\!?(count|files|has|has-dir|hasraw|matches|matchesraw|snapshot)\\s.*"
210 ).is_match(line)
211 || static_regex!(
213 "\\s*// [a-zA-Z0-9-_]*:\\s.*"
214 ).is_match(line)
215}
216
217fn long_line_is_ok(extension: &str, is_error_code: bool, max_columns: usize, line: &str) -> bool {
219 match extension {
220 "md" if !is_error_code => true,
222 "md" if line == INTERNAL_COMPILER_DOCS_LINE => true,
224 _ => line_is_url(is_error_code, max_columns, line) || should_ignore(line),
225 }
226}
227
228macro_rules! suppressible_tidy_err {
229 ($err:ident, $skip:expr, $msg:literal) => {
230 if let Err(()) = $skip.check() {
231 $err(&format!($msg));
232 }
233 };
234}
235
236pub fn is_in(full_path: &Path, parent_folder_to_find: &str, folder_to_find: &str) -> bool {
237 if let Some(parent) = full_path.parent() {
238 if parent.file_name().map_or_else(
239 || false,
240 |f| {
241 f == folder_to_find
242 && parent
243 .parent()
244 .and_then(|f| f.file_name())
245 .map_or_else(|| false, |f| f == parent_folder_to_find)
246 },
247 ) {
248 true
249 } else {
250 is_in(parent, parent_folder_to_find, folder_to_find)
251 }
252 } else {
253 false
254 }
255}
256
257fn skip_markdown_path(path: &Path) -> bool {
258 const SKIP_MD: &[&str] = &[
260 "src/doc/edition-guide",
261 "src/doc/embedded-book",
262 "src/doc/nomicon",
263 "src/doc/reference",
264 "src/doc/rust-by-example",
265 "src/doc/rustc-dev-guide",
266 ];
267 SKIP_MD.iter().any(|p| path.ends_with(p))
268}
269
270fn is_unexplained_ignore(extension: &str, line: &str) -> bool {
271 if !line.ends_with("```ignore") && !line.ends_with("```rust,ignore") {
272 return false;
273 }
274 if extension == "md" && line.trim().starts_with("//") {
275 return false;
278 }
279 true
280}
281
282pub fn check(path: &Path, tidy_ctx: TidyCtx) {
283 let mut check = tidy_ctx.start_check(CheckId::new("style").path(path));
284
285 fn skip(path: &Path, is_dir: bool) -> bool {
286 if path.file_name().is_some_and(|name| name.to_string_lossy().starts_with(".#")) {
287 return true;
289 }
290
291 if filter_dirs(path) || skip_markdown_path(path) {
292 return true;
293 }
294
295 if is_dir {
297 return false;
298 }
299
300 let extensions = ["rs", "py", "js", "sh", "c", "cpp", "h", "md", "css", "goml"];
301
302 if path.extension().is_none_or(|ext| !extensions.iter().any(|e| ext == OsStr::new(e))) {
304 return true;
305 }
306
307 path.extension().is_some_and(|e| e == "css") && !is_in(path, "src", "librustdoc")
309 }
310
311 walk(path, skip, &mut |entry, contents| {
312 let file = entry.path();
313 check_file_style(&mut check, file, contents);
314 });
315}
316
317static PROBLEMATIC_REGEX: LazyLock<regex::RegexSet> = LazyLock::new(|| {
320 RegexSetBuilder::new(PROBLEMATIC_CONSTS_STRINGS.as_slice())
321 .case_insensitive(true)
322 .build()
323 .unwrap()
324});
325
326fn check_file_style(check: &mut RunningCheck, file: &Path, contents: &str) {
327 let this_file = Path::new(file!());
330 let codegen_file = Path::new("src/tools/tidy/src/codegen.rs");
331
332 let path_str = file.to_string_lossy();
333 let filename = file.file_name().unwrap().to_string_lossy();
334
335 let is_css_file = filename.ends_with(".css");
336 let under_rustfmt = filename.ends_with(".rs") &&
337 !file.ancestors().any(|a| {
340 (a.ends_with("tests") && a.join("COMPILER_TESTS.md").exists()) ||
341 a.ends_with("src/doc/book")
342 });
343
344 if contents.is_empty() {
345 check.error(format!("{}: empty file", file.display()));
346 }
347
348 let extension = file.extension().unwrap().to_string_lossy();
349 let is_error_code = extension == "md" && is_in(file, "src", "error_codes");
350 let is_goml_code = extension == "goml";
351
352 let max_columns = if is_error_code {
353 ERROR_CODE_COLS
354 } else if is_goml_code {
355 GOML_COLS
356 } else {
357 COLS
358 };
359
360 let can_contain = match_ignore(contents, false, None) || match_ignore(contents, true, None);
362
363 if filename.contains("ignore-tidy") {
366 return;
367 }
368 if let Some(p) = file.parent()
370 && p.ends_with(Path::new("src/etc/completions"))
371 {
372 return;
373 }
374
375 let file_ignore = Directives::from_str(&path_str, LineNumber::WholeFile, can_contain, contents);
376 let mut next_line_ignore = Default::default();
378
379 if file_ignore.all.is_ignore_and_defuse() {
381 file_ignore.iter().for_each(|i| i.force_discard_unsused_ignore());
382 return;
383 }
384
385 let mut leading_new_lines = false;
386 let mut trailing_new_lines = 0;
387 let mut lines = 0;
388 let mut last_safety_comment = false;
389 let mut comment_block: Option<(usize, usize, NamedDirective)> = None;
390 let is_test =
391 file.components().any(|c| c.as_os_str() == "tests") || file.file_stem().unwrap() == "tests";
392 let is_codegen_test = is_test && file.components().any(|c| c.as_os_str() == "codegen-llvm");
393 let is_this_file = file.ends_with(this_file) || this_file.ends_with(file);
394 let is_test_for_this_file =
395 is_test && file.parent().unwrap().ends_with(this_file.with_extension(""));
396 let is_codegen_tidy_file = file.ends_with(codegen_file);
397 let any_problematic_line =
400 !is_this_file && !is_test_for_this_file && PROBLEMATIC_REGEX.is_match(contents);
401 for (i, line) in contents.split('\n').enumerate() {
402 if line.is_empty() {
403 if i == 0 {
404 leading_new_lines = true;
405 }
406 trailing_new_lines += 1;
407 continue;
408 } else {
409 trailing_new_lines = 0;
410 }
411
412 let line_number = i + 1;
413
414 let mut ignore = file_ignore.create_child(
415 mem::replace(
416 &mut next_line_ignore,
417 Directives::from_str(&path_str, LineNumber::Line(line_number), can_contain, line),
418 ),
419 check,
420 file,
421 );
422
423 let trimmed = line.trim();
424
425 if !trimmed.starts_with("//") {
426 lines += 1;
427 }
428
429 let mut err = |msg: &str| {
430 check.error(format!("{}:{}: {msg}", file.display(), line_number));
431 };
432
433 if !is_this_file
434 && trimmed.contains("dbg!")
435 && !trimmed.starts_with("//")
436 && !file.ancestors().any(|a| {
437 (a.ends_with("tests") && a.join("COMPILER_TESTS.md").exists())
438 || a.ends_with("library/alloctests")
439 })
440 && filename != "tests.rs"
441 {
442 suppressible_tidy_err!(
443 err,
444 ignore.dbg,
445 "`dbg!` macro is intended as a debugging tool. It should not be in version control."
446 )
447 }
448
449 if !is_this_file
450 && trimmed.contains("todo!")
451 && !trimmed.starts_with("//")
452 && !file.ancestors().any(|a| {
453 (a.ends_with("tests") && a.join("COMPILER_TESTS.md").exists())
454 || a.ends_with("library/alloctests")
455 })
456 && filename != "tests.rs"
457 {
458 suppressible_tidy_err!(
459 err,
460 ignore.todo,
461 "the `todo!` macro is used for tasks that should be done before merging a PR. If you want to panic here, use `panic!`, `unimplemented!`, `unreachable!`, `rustc_middle::bug!` or an assertion"
462 )
463 }
464
465 if is_codegen_test && trimmed.contains("CHECK") && trimmed.ends_with(": br") {
466 err("`CHECK: br` and `CHECK-NOT: br` in codegen tests are fragile to false \
467 positives in mangled symbols. Try using `br {{.*}}` instead.")
468 }
469
470 if !under_rustfmt
471 && line.chars().count() > max_columns
472 && !long_line_is_ok(&extension, is_error_code, max_columns, line)
473 {
474 suppressible_tidy_err!(err, ignore.linelength, "line longer than {max_columns} chars");
475 }
476 if !is_css_file && line.contains('\t') {
477 suppressible_tidy_err!(err, ignore.tab, "tab character");
478 }
479 if line.ends_with(' ') || line.ends_with('\t') {
480 suppressible_tidy_err!(err, ignore.end_whitespace, "trailing whitespace");
481 }
482 if is_css_file && line.starts_with(' ') {
483 err("CSS files use tabs for indent");
484 }
485 if line.contains('\r') {
486 suppressible_tidy_err!(err, ignore.cr, "CR character");
487 }
488 if !is_this_file && !is_codegen_tidy_file {
489 let directive_line_starts = ["// ", "# ", "/* ", "<!-- "];
490 let possible_line_start =
491 directive_line_starts.into_iter().any(|s| line.starts_with(s));
492 let contains_potential_directive =
493 possible_line_start && (line.contains("-tidy") || line.contains("tidy-"));
494 let has_recognized_ignore_directive = can_contain
495 && (Directives::parse(LineNumber::Line(line_number), line)
496 .iter()
497 .any(|directive| directive.is_ignore_and_defuse())
498 || Directives::parse(LineNumber::WholeFile, line)
499 .iter()
500 .any(|directive| directive.is_ignore_and_defuse()));
501 let has_alphabetical_directive =
502 line.contains("tidy-alphabetical-start") || line.contains("tidy-alphabetical-end");
503 let has_other_tidy_ignore_directive =
504 line.contains("ignore-tidy-target-specific-tests");
505 let has_recognized_directive = has_recognized_ignore_directive
506 || has_alphabetical_directive
507 || has_other_tidy_ignore_directive;
508 if contains_potential_directive && (!has_recognized_directive) {
509 err("Unrecognized tidy directive")
510 }
511 if trimmed.contains("TODO") && !trimmed.contains("ignore-tidy-todo") {
514 err(
515 "TODO is used for tasks that should be done before merging a PR; If you want to leave a message in the codebase use FIXME",
516 )
517 }
518 if trimmed.contains("//") && trimmed.contains(" XXX") {
519 err("Instead of XXX use FIXME")
520 }
521 if any_problematic_line && contains_problematic_const(trimmed) {
522 err("Don't use magic numbers that spell things (consider 0x12345678)");
523 }
524 }
525 if trimmed.contains("unsafe {")
527 && !trimmed.starts_with("//")
528 && !last_safety_comment
529 && file.components().any(|c| c.as_os_str() == "core")
530 && !is_test
531 {
532 suppressible_tidy_err!(err, ignore.undocumented_unsafe, "undocumented unsafe");
533 }
534 if trimmed.contains("// SAFETY:") {
535 last_safety_comment = true;
536 } else if trimmed.starts_with("//") || trimmed.is_empty() {
537 } else {
539 last_safety_comment = false;
540 }
541 if (line.starts_with("// Copyright")
542 || line.starts_with("# Copyright")
543 || line.starts_with("Copyright"))
544 && (trimmed.contains("Rust Developers") || trimmed.contains("Rust Project Developers"))
545 {
546 suppressible_tidy_err!(
547 err,
548 ignore.copyright,
549 "copyright notices attributed to the Rust Project Developers are deprecated"
550 );
551 }
552 if !file.components().any(|c| c.as_os_str() == "rustc_baked_icu_data")
553 && is_unexplained_ignore(&extension, line)
554 {
555 err(UNEXPLAINED_IGNORE_DOCTEST_INFO);
556 }
557
558 if filename.ends_with(".cpp") && line.contains("llvm_unreachable") {
559 err(LLVM_UNREACHABLE_INFO);
560 }
561
562 let is_compiler = || file.components().any(|c| c.as_os_str() == "compiler");
564
565 if is_compiler() {
566 if line.contains("//")
567 && line
568 .chars()
569 .collect::<Vec<_>>()
570 .windows(4)
571 .any(|cs| matches!(cs, ['.', ' ', ' ', last] if last.is_alphabetic()))
572 {
573 err(DOUBLE_SPACE_AFTER_DOT)
574 }
575
576 let likely_comment = |trimmed: &str| {
580 if trimmed.contains("ignore-tidy") {
581 return false;
582 }
583
584 trimmed.contains("//")
586 || (trimmed.contains("cfg_attr") && trimmed.contains("doc"))
588 };
589
590 if likely_comment(trimmed) {
591 let (start_line, mut backtick_count, directive) =
592 comment_block.take().unwrap_or((line_number, 0, ignore.odd_backticks.take()));
593 let line_backticks = trimmed.chars().filter(|ch| *ch == '`').count();
594
595 let comment_text = match trimmed.split("//").nth(1) {
598 Some(text) => text,
599 None => {
600 let (_doc, rest) =
602 trimmed.split_once("doc").expect("failed to find `doc` attribute");
603 rest
604 }
605 };
606
607 if line_backticks % 2 == 1 {
610 backtick_count += comment_text.chars().filter(|ch| *ch == '`').count();
611 }
612 comment_block = Some((start_line, backtick_count, directive));
613 } else if let Some((start_line, backtick_count, directive)) = comment_block.take()
614 && backtick_count % 2 == 1
615 {
616 let mut err = |msg: &str| {
617 check.error(format!("{}:{start_line}: {msg}", file.display()));
618 };
619 let block_len = line_number - start_line;
620 if block_len == 1 {
621 suppressible_tidy_err!(
622 err,
623 directive,
625 "comment with odd number of backticks"
626 );
627 } else {
628 suppressible_tidy_err!(
629 err,
630 directive,
632 "{block_len}-line comment block with odd number of backticks"
633 );
634 }
635
636 directive.check_usage(check, file);
637 }
638 }
639
640 ignore.check_usage(check, file);
641 }
642 if leading_new_lines {
643 let mut err = |_| {
644 check.error(format!("{}: leading newline", file.display()));
645 };
646 suppressible_tidy_err!(err, file_ignore.leading_newlines, "missing leading newline");
647 }
648 let mut err = |msg: &str| {
649 check.error(format!("{}: {}", file.display(), msg));
650 };
651 match trailing_new_lines {
652 0 => suppressible_tidy_err!(err, file_ignore.trailing_newlines, "missing trailing newline"),
653 1 => {}
654 n => suppressible_tidy_err!(
655 err,
656 file_ignore.trailing_newlines,
657 "too many trailing newlines ({n})"
658 ),
659 };
660 if lines > LINES {
661 let mut err = |_| {
662 check.error(format!(
663 "{}: too many lines ({lines}) (add `// \
664 ignore-tidy-file-filelength` to the file to suppress this error)",
665 file.display(),
666 ));
667 };
668 suppressible_tidy_err!(err, file_ignore.filelength, "");
669 }
670
671 if let Some((_, _, directive)) = comment_block.take() {
672 directive.check_usage(check, file);
673 }
674 drop(comment_block);
675 next_line_ignore.check_usage(check, file);
676 file_ignore.check_usage(check, file);
677}