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 "//@ revisions",
78];
79
80fn generate_problems<'a>(
81 consts: &'a [u32],
82 letter_digit: &'a FxHashMap<char, char>,
83) -> impl Iterator<Item = u32> + 'a {
84 consts.iter().flat_map(move |const_value| {
85 let problem = letter_digit.iter().fold(format!("{const_value:X}"), |acc, (key, value)| {
86 acc.replace(&value.to_string(), &key.to_string())
87 });
88 let indexes: Vec<usize> = problem
89 .chars()
90 .enumerate()
91 .filter_map(|(index, c)| if letter_digit.contains_key(&c) { Some(index) } else { None })
92 .collect();
93 (0..1 << indexes.len()).map(move |i| {
94 u32::from_str_radix(
95 &problem
96 .chars()
97 .enumerate()
98 .map(|(index, c)| {
99 if let Some(pos) = indexes.iter().position(|&x| x == index) {
100 if (i >> pos) & 1 == 1 { letter_digit[&c] } else { c }
101 } else {
102 c
103 }
104 })
105 .collect::<String>(),
106 0x10,
107 )
108 .unwrap()
109 })
110 })
111}
112
113const ROOT_PROBLEMATIC_CONSTS: &[u32] = &[
115 184594741, 2880289470, 2881141438, 2965027518, 2976579765, 3203381950, 3405691582, 3405697037,
116 3735927486, 3735932941, 4027431614, 4276992702, 195934910, 252707358, 762133, 179681982,
117 173390526, 721077,
118];
119
120const LETTER_DIGIT: &[(char, char)] = &[('A', '4'), ('B', '8'), ('E', '3')];
121
122fn generate_problematic_strings(
124 consts: &[u32],
125 letter_digit: &FxHashMap<char, char>,
126) -> Vec<String> {
127 generate_problems(consts, letter_digit)
128 .flat_map(|v| vec![v.to_string(), format!("{:X}", v)])
129 .collect()
130}
131
132static PROBLEMATIC_CONSTS_STRINGS: LazyLock<Vec<String>> = LazyLock::new(|| {
133 generate_problematic_strings(ROOT_PROBLEMATIC_CONSTS, &LETTER_DIGIT.iter().cloned().collect())
134});
135
136fn contains_problematic_const(trimmed: &str) -> bool {
137 PROBLEMATIC_CONSTS_STRINGS.iter().any(|s| trimmed.to_uppercase().contains(s))
138}
139
140const INTERNAL_COMPILER_DOCS_LINE: &str = "#### This error code is internal to the compiler and will not be emitted with normal Rust code.";
141
142#[derive(Clone, Copy, PartialEq)]
144#[allow(non_camel_case_types)]
145enum LIUState {
146 EXP_COMMENT_START,
147 EXP_LINK_LABEL_OR_URL,
148 EXP_URL,
149 EXP_END,
150}
151
152fn line_is_url(is_error_code: bool, columns: usize, line: &str) -> bool {
159 if is_error_code {
161 return line.starts_with('[') && line.contains("]:") && line.contains("http");
162 }
163
164 use self::LIUState::*;
165 let mut state: LIUState = EXP_COMMENT_START;
166 let is_url = |w: &str| w.starts_with("http://") || w.starts_with("https://");
167
168 for tok in line.split_whitespace() {
169 match (state, tok) {
170 (EXP_COMMENT_START, "//") | (EXP_COMMENT_START, "///") | (EXP_COMMENT_START, "//!") => {
171 state = EXP_LINK_LABEL_OR_URL
172 }
173
174 (EXP_LINK_LABEL_OR_URL, w)
175 if w.len() >= 4 && w.starts_with('[') && w.ends_with("]:") =>
176 {
177 state = EXP_URL
178 }
179
180 (EXP_LINK_LABEL_OR_URL, w) if is_url(w) => state = EXP_END,
181
182 (EXP_URL, w) if is_url(w) || w.starts_with("../") => state = EXP_END,
183
184 (_, w) if w.len() > columns && is_url(w) => state = EXP_END,
185
186 (_, _) => {}
187 }
188 }
189
190 state == EXP_END
191}
192
193fn should_ignore(line: &str) -> bool {
196 static_regex!("\\s*//(\\[.*\\])?~.*").is_match(line)
200 || ANNOTATIONS_TO_IGNORE.iter().any(|a| line.contains(a))
201
202 || static_regex!("\\s*//@(\\[.*\\]) (compile-flags|normalize-stderr|error-pattern).*")
206 .is_match(line)
207 || static_regex!(
210 "\\s*//@ \\!?(count|files|has|has-dir|hasraw|matches|matchesraw|snapshot)\\s.*"
211 ).is_match(line)
212 || static_regex!(
214 "\\s*// [a-zA-Z0-9-_]*:\\s.*"
215 ).is_match(line)
216}
217
218fn long_line_is_ok(extension: &str, is_error_code: bool, max_columns: usize, line: &str) -> bool {
220 match extension {
221 "md" if !is_error_code => true,
223 "md" if line == INTERNAL_COMPILER_DOCS_LINE => true,
225 _ => line_is_url(is_error_code, max_columns, line) || should_ignore(line),
226 }
227}
228
229macro_rules! suppressible_tidy_err {
230 ($err:ident, $skip:expr, $msg:literal $($args: tt)*) => {
231 if let Err(()) = $skip.check() {
232 $err(&format!($msg $($args)*));
233 }
234 };
235}
236
237pub fn is_in(full_path: &Path, parent_folder_to_find: &str, folder_to_find: &str) -> bool {
238 if let Some(parent) = full_path.parent() {
239 if parent.file_name().map_or_else(
240 || false,
241 |f| {
242 f == folder_to_find
243 && parent
244 .parent()
245 .and_then(|f| f.file_name())
246 .map_or_else(|| false, |f| f == parent_folder_to_find)
247 },
248 ) {
249 true
250 } else {
251 is_in(parent, parent_folder_to_find, folder_to_find)
252 }
253 } else {
254 false
255 }
256}
257
258fn skip_markdown_path(path: &Path) -> bool {
259 const SKIP_MD: &[&str] = &[
261 "src/doc/edition-guide",
262 "src/doc/embedded-book",
263 "src/doc/nomicon",
264 "src/doc/reference",
265 "src/doc/rust-by-example",
266 "src/doc/rustc-dev-guide",
267 ];
268 SKIP_MD.iter().any(|p| path.ends_with(p))
269}
270
271fn is_unexplained_ignore(extension: &str, line: &str) -> bool {
272 if !line.ends_with("```ignore") && !line.ends_with("```rust,ignore") {
273 return false;
274 }
275 if extension == "md" && line.trim().starts_with("//") {
276 return false;
279 }
280 true
281}
282
283pub fn check(path: &Path, tidy_ctx: TidyCtx) {
284 let mut check = tidy_ctx.start_check(CheckId::new("style").path(path));
285
286 fn skip(path: &Path, is_dir: bool) -> bool {
287 if path.file_name().is_some_and(|name| name.to_string_lossy().starts_with(".#")) {
288 return true;
290 }
291
292 if filter_dirs(path) || skip_markdown_path(path) {
293 return true;
294 }
295
296 if is_dir {
298 return false;
299 }
300
301 let extensions = ["rs", "py", "js", "sh", "c", "cpp", "h", "md", "css", "goml"];
302
303 if path.extension().is_none_or(|ext| !extensions.iter().any(|e| ext == OsStr::new(e))) {
305 return true;
306 }
307
308 path.extension().is_some_and(|e| e == "css") && !is_in(path, "src", "librustdoc")
310 }
311
312 walk(path, skip, &mut |entry, contents| {
313 let file = entry.path();
314 check_file_style(path, &mut check, file, contents);
315 });
316}
317
318static PROBLEMATIC_REGEX: LazyLock<regex::RegexSet> = LazyLock::new(|| {
321 RegexSetBuilder::new(PROBLEMATIC_CONSTS_STRINGS.as_slice())
322 .case_insensitive(true)
323 .build()
324 .unwrap()
325});
326
327fn check_file_style(base_path: &Path, check: &mut RunningCheck, file: &Path, contents: &str) {
328 let this_file = Path::new(file!());
331 let codegen_file = Path::new("src/tools/tidy/src/codegen.rs");
332
333 let path_str = file.to_string_lossy();
334 let filename = file.file_name().unwrap().to_string_lossy();
335
336 let is_css_file = filename.ends_with(".css");
337 let under_rustfmt = filename.ends_with(".rs") &&
338 !file.ancestors().any(|a| {
341 (a.ends_with("tests") && a.join("COMPILER_TESTS.md").exists()) ||
342 a.ends_with("src/doc/book")
343 });
344
345 if contents.is_empty() {
346 if file.file_name().is_some_and(|x| x == "__init__.py") {
349 return;
350 }
351 check.error(format!("{}: empty file", file.display()));
352 }
353
354 let extension = file.extension().unwrap().to_string_lossy();
355 let is_error_code = extension == "md" && is_in(file, "src", "error_codes");
356 let is_goml_code = extension == "goml";
357
358 let max_columns = if is_error_code {
359 ERROR_CODE_COLS
360 } else if is_goml_code {
361 GOML_COLS
362 } else {
363 COLS
364 };
365
366 let can_contain = match_ignore(contents, false, None) || match_ignore(contents, true, None);
368
369 if filename.contains("ignore-tidy") {
372 return;
373 }
374 if let Some(p) = file.parent()
376 && p.ends_with(Path::new("src/etc/completions"))
377 {
378 return;
379 }
380
381 let file_ignore = Directives::from_str(&path_str, LineNumber::WholeFile, can_contain, contents);
382 let mut next_line_ignore = Default::default();
384
385 if file_ignore.all.is_ignore_and_defuse() {
387 file_ignore.iter().for_each(|i| i.force_discard_unsused_ignore());
388 return;
389 }
390
391 let mut leading_new_lines = false;
392 let mut trailing_new_lines = 0;
393 let mut lines = 0;
394 let mut last_safety_comment = false;
395 let mut comment_block: Option<(usize, usize, NamedDirective)> = None;
396 let is_test =
397 file.components().any(|c| c.as_os_str() == "tests") || file.file_stem().unwrap() == "tests";
398 let is_codegen_test = is_test && file.components().any(|c| c.as_os_str() == "codegen-llvm");
399 let is_this_file = file.ends_with(this_file) || this_file.ends_with(file);
400 let is_test_for_this_file =
401 is_test && file.parent().unwrap().ends_with(this_file.with_extension(""));
402 let is_codegen_tidy_file = file.ends_with(codegen_file);
403 let any_problematic_line =
406 !is_this_file && !is_test_for_this_file && PROBLEMATIC_REGEX.is_match(contents);
407 for (i, line) in contents.split('\n').enumerate() {
408 if line.is_empty() {
409 if i == 0 {
410 leading_new_lines = true;
411 }
412 trailing_new_lines += 1;
413 continue;
414 } else {
415 trailing_new_lines = 0;
416 }
417
418 let line_number = i + 1;
419
420 let mut ignore = file_ignore.create_child(
421 mem::replace(
422 &mut next_line_ignore,
423 Directives::from_str(&path_str, LineNumber::Line(line_number), can_contain, line),
424 ),
425 check,
426 file,
427 );
428
429 let trimmed = line.trim();
430
431 if !trimmed.starts_with("//") {
432 lines += 1;
433 }
434
435 let mut err = |msg: &str| {
436 check.error(format!("{}:{}: {msg}", file.display(), line_number));
437 };
438
439 if !is_this_file
440 && trimmed.contains("dbg!")
441 && !trimmed.starts_with("//")
442 && !file.ancestors().any(|a| {
443 (a.ends_with("tests") && a.join("COMPILER_TESTS.md").exists())
444 || a.ends_with("library/alloctests")
445 })
446 && filename != "tests.rs"
447 {
448 suppressible_tidy_err!(
449 err,
450 ignore.dbg,
451 "`dbg!` macro is intended as a debugging tool. It should not be in version control."
452 )
453 }
454
455 if !is_this_file
456 && trimmed.contains("todo!")
457 && !trimmed.starts_with("//")
458 && !file.ancestors().any(|a| {
459 (a.ends_with("tests") && a.join("COMPILER_TESTS.md").exists())
460 || a.ends_with("library/alloctests")
461 })
462 && filename != "tests.rs"
463 {
464 let without_macro_call =
465 trimmed.split_once("todo!").expect("todo in line because of previous check").1;
466 let without_start =
467 without_macro_call.split_once("(").map_or(without_macro_call, |(_, s)| s);
468 let without_end =
469 without_start.rsplit_once(")").map_or(without_start, |(s, _)| s).trim();
470 let message = if without_end.is_empty() {
471 format_args!("")
472 } else {
473 format_args!("\n >> TODO: {}", without_end)
474 };
475
476 suppressible_tidy_err!(
477 err,
478 ignore.todo,
479 "the `todo!` macro is used for tasks that should be done before merging a PR.\nIf you want to panic here, use `panic!`, `unimplemented!`, `unreachable!`, `rustc_middle::bug!` or an assertion{message}"
480 )
481 }
482
483 if is_codegen_test && trimmed.contains("CHECK") && trimmed.ends_with(": br") {
484 err("`CHECK: br` and `CHECK-NOT: br` in codegen tests are fragile to false \
485 positives in mangled symbols. Try using `br {{.*}}` instead.")
486 }
487
488 if !under_rustfmt
489 && line.chars().count() > max_columns
490 && !long_line_is_ok(&extension, is_error_code, max_columns, line)
491 {
492 suppressible_tidy_err!(err, ignore.linelength, "line longer than {max_columns} chars");
493 }
494 if !is_css_file && line.contains('\t') {
495 suppressible_tidy_err!(err, ignore.tab, "tab character");
496 }
497 if line.ends_with(' ') || line.ends_with('\t') {
498 suppressible_tidy_err!(err, ignore.end_whitespace, "trailing whitespace");
499 }
500 if is_css_file && line.starts_with(' ') {
501 err("CSS files use tabs for indent");
502 }
503 if line.contains('\r') {
504 suppressible_tidy_err!(err, ignore.cr, "CR character");
505 }
506 if !is_this_file && !is_codegen_tidy_file {
507 let directive_line_starts = ["// ", "# ", "/* ", "<!-- "];
508 let possible_line_start =
509 directive_line_starts.into_iter().any(|s| line.starts_with(s));
510 let contains_potential_directive =
511 possible_line_start && (line.contains("-tidy") || line.contains("tidy-"));
512 let has_recognized_ignore_directive = can_contain
513 && (Directives::parse(LineNumber::Line(line_number), line)
514 .iter()
515 .any(|directive| directive.is_ignore_and_defuse())
516 || Directives::parse(LineNumber::WholeFile, line)
517 .iter()
518 .any(|directive| directive.is_ignore_and_defuse()));
519 let has_alphabetical_directive =
520 line.contains("tidy-alphabetical-start") || line.contains("tidy-alphabetical-end");
521 let has_other_tidy_ignore_directive =
522 line.contains("ignore-tidy-target-specific-tests");
523 let has_recognized_directive = has_recognized_ignore_directive
524 || has_alphabetical_directive
525 || has_other_tidy_ignore_directive;
526 if contains_potential_directive && (!has_recognized_directive) {
527 err("Unrecognized tidy directive")
528 }
529 if trimmed.contains("TODO") {
530 let without_todo =
531 trimmed.split_once("TODO").expect("TODO in line because of previous check").1;
532 let without_colon = without_todo.trim().trim_start_matches(":").trim();
533
534 let message = if without_colon.is_empty() {
535 format_args!("")
536 } else {
537 format_args!("\n >> TODO: {}", without_colon)
538 };
539 suppressible_tidy_err!(
540 err,
541 ignore.todo,
542 "TODO is used for tasks that should be done before merging a PR;\nIf you want to leave a message in the codebase use FIXME{message}",
543 )
544 }
545 if trimmed.contains("//") && trimmed.contains(" XXX") {
546 err("Instead of XXX use FIXME")
547 }
548 if any_problematic_line && contains_problematic_const(trimmed) {
549 err("Don't use magic numbers that spell things (consider 0x12345678)");
550 }
551 }
552 if trimmed.contains("unsafe {")
555 && !trimmed.starts_with("//")
556 && !last_safety_comment
557 && !is_test
558 && base_path.ends_with("library")
559 && file
560 .strip_prefix(base_path)
561 .is_ok_and(|rel| rel.starts_with("core") || rel.starts_with("alloc"))
562 {
563 suppressible_tidy_err!(err, ignore.undocumented_unsafe, "undocumented unsafe");
564 }
565 if trimmed.contains("// SAFETY:") {
566 last_safety_comment = true;
567 } else if trimmed.starts_with("//") || trimmed.is_empty() {
568 } else {
570 last_safety_comment = false;
571 }
572 if (line.starts_with("// Copyright")
573 || line.starts_with("# Copyright")
574 || line.starts_with("Copyright"))
575 && (trimmed.contains("Rust Developers") || trimmed.contains("Rust Project Developers"))
576 {
577 suppressible_tidy_err!(
578 err,
579 ignore.copyright,
580 "copyright notices attributed to the Rust Project Developers are deprecated"
581 );
582 }
583 if !file.components().any(|c| c.as_os_str() == "rustc_baked_icu_data")
584 && is_unexplained_ignore(&extension, line)
585 {
586 err(UNEXPLAINED_IGNORE_DOCTEST_INFO);
587 }
588
589 if filename.ends_with(".cpp") && line.contains("llvm_unreachable") {
590 err(LLVM_UNREACHABLE_INFO);
591 }
592
593 let is_compiler = || file.components().any(|c| c.as_os_str() == "compiler");
595
596 if is_compiler() {
597 if line.contains("//")
598 && line
599 .chars()
600 .collect::<Vec<_>>()
601 .windows(4)
602 .any(|cs| matches!(cs, ['.', ' ', ' ', last] if last.is_alphabetic()))
603 {
604 err(DOUBLE_SPACE_AFTER_DOT)
605 }
606
607 let likely_comment = |trimmed: &str| {
611 if trimmed.contains("ignore-tidy") {
612 return false;
613 }
614
615 trimmed.contains("//")
617 || (trimmed.contains("cfg_attr") && trimmed.contains("doc"))
619 };
620
621 if likely_comment(trimmed) {
622 let (start_line, mut backtick_count, directive) =
623 comment_block.take().unwrap_or((line_number, 0, ignore.odd_backticks.take()));
624 let line_backticks = trimmed.chars().filter(|ch| *ch == '`').count();
625
626 let comment_text = match trimmed.split("//").nth(1) {
629 Some(text) => text,
630 None => {
631 let (_doc, rest) =
633 trimmed.split_once("doc").expect("failed to find `doc` attribute");
634 rest
635 }
636 };
637
638 if line_backticks % 2 == 1 {
641 backtick_count += comment_text.chars().filter(|ch| *ch == '`').count();
642 }
643 comment_block = Some((start_line, backtick_count, directive));
644 } else if let Some((start_line, backtick_count, directive)) = comment_block.take()
645 && backtick_count % 2 == 1
646 {
647 let mut err = |msg: &str| {
648 check.error(format!("{}:{start_line}: {msg}", file.display()));
649 };
650 let block_len = line_number - start_line;
651 if block_len == 1 {
652 suppressible_tidy_err!(
653 err,
654 directive,
656 "comment with odd number of backticks"
657 );
658 } else {
659 suppressible_tidy_err!(
660 err,
661 directive,
663 "{block_len}-line comment block with odd number of backticks"
664 );
665 }
666
667 directive.check_usage(check, file);
668 }
669 }
670
671 ignore.check_usage(check, file);
672 }
673 if leading_new_lines {
674 let mut err = |_| {
675 check.error(format!("{}: leading newline", file.display()));
676 };
677 suppressible_tidy_err!(err, file_ignore.leading_newlines, "missing leading newline");
678 }
679 let mut err = |msg: &str| {
680 check.error(format!("{}: {}", file.display(), msg));
681 };
682 match trailing_new_lines {
683 0 => suppressible_tidy_err!(err, file_ignore.trailing_newlines, "missing trailing newline"),
684 1 => {}
685 n => suppressible_tidy_err!(
686 err,
687 file_ignore.trailing_newlines,
688 "too many trailing newlines ({n})"
689 ),
690 };
691 if lines > LINES {
692 let mut err = |_| {
693 check.error(format!(
694 "{}: too many lines ({lines}) (add `// \
695 ignore-tidy-file-filelength` to the file to suppress this error)",
696 file.display(),
697 ));
698 };
699 suppressible_tidy_err!(err, file_ignore.filelength, "");
700 }
701
702 if let Some((_, _, directive)) = comment_block.take() {
703 directive.check_usage(check, file);
704 }
705 drop(comment_block);
706 next_line_ignore.check_usage(check, file);
707 file_ignore.check_usage(check, file);
708}