1 //! Tidy check to enforce various stylistic guidelines on the Rust codebase.
3 //! Example checks are:
5 //! * No lines over 100 characters (in non-Rust files).
6 //! * No files with over 3000 lines (in non-Rust files).
8 //! * No trailing whitespace.
9 //! * No CR characters.
10 //! * No `TODO` or `XXX` directives.
11 //! * No unexplained ` ```ignore ` or ` ```rust,ignore ` doc tests.
13 //! Note that some of these rules are excluded from Rust files because we enforce rustfmt. It is
14 //! preferable to be formatted rather than tidy-clean.
16 //! A number of these checks can be opted-out of with various directives of the form:
17 //! `// ignore-tidy-CHECK-NAME`.
22 /// Error code markdown is restricted to 80 columns because they can be
23 /// displayed on the console with --example.
24 const ERROR_CODE_COLS
: usize = 80;
25 const COLS
: usize = 100;
27 const LINES
: usize = 3000;
29 const UNEXPLAINED_IGNORE_DOCTEST_INFO
: &str = r
#"unexplained "```ignore" doctest; try one:
31 * make the test actually pass, by adding necessary imports and declarations, or
32 * use "```text", if the code is not Rust code, or
33 * use "```compile_fail,Ennnn", if the code is expected to fail at compile time, or
34 * use "```should_panic", if the code is expected to fail at run time, or
35 * use "```no_run", if the code should type-check but not necessary linkable/runnable, or
36 * explain it like "```ignore (cannot-test-this-because-xxxx)", if the annotation cannot be avoided.
40 const LLVM_UNREACHABLE_INFO
: &str = r
"\
41 C++ code used llvm_unreachable, which triggers undefined behavior
42 when executed when assertions are disabled.
43 Use llvm::report_fatal_error for increased robustness.";
45 const ANNOTATIONS_TO_IGNORE
: &[&str] = &[
56 "// normalize-stderr-test",
59 // Intentionally written in decimal rather than hex
60 const PROBLEMATIC_CONSTS
: &[u32] = &[
61 184594741, 2880289470, 2881141438, 2965027518, 2976579765, 3203381950, 3405691582, 3405697037,
62 3735927486, 4027431614, 4276992702,
65 /// Parser states for `line_is_url`.
66 #[derive(Clone, Copy, PartialEq)]
67 #[allow(non_camel_case_types)]
70 EXP_LINK_LABEL_OR_URL
,
75 /// Returns `true` if `line` appears to be a line comment containing a URL,
76 /// possibly with a Markdown link label in front, and nothing else.
77 /// The Markdown link label, if present, may not contain whitespace.
78 /// Lines of this form are allowed to be overlength, because Markdown
79 /// offers no way to split a line in the middle of a URL, and the lengths
80 /// of URLs to external references are beyond our control.
81 fn line_is_url(is_error_code
: bool
, columns
: usize, line
: &str) -> bool
{
82 // more basic check for markdown, to avoid complexity in implementing two state machines
84 return line
.starts_with('
['
) && line
.contains("]:") && line
.contains("http");
87 use self::LIUState
::*;
88 let mut state
: LIUState
= EXP_COMMENT_START
;
89 let is_url
= |w
: &str| w
.starts_with("http://") || w
.starts_with("https://");
91 for tok
in line
.split_whitespace() {
93 (EXP_COMMENT_START
, "//") | (EXP_COMMENT_START
, "///") | (EXP_COMMENT_START
, "//!") => {
94 state
= EXP_LINK_LABEL_OR_URL
97 (EXP_LINK_LABEL_OR_URL
, w
)
98 if w
.len() >= 4 && w
.starts_with('
['
) && w
.ends_with("]:") =>
103 (EXP_LINK_LABEL_OR_URL
, w
) if is_url(w
) => state
= EXP_END
,
105 (EXP_URL
, w
) if is_url(w
) || w
.starts_with("../") => state
= EXP_END
,
107 (_
, w
) if w
.len() > columns
&& is_url(w
) => state
= EXP_END
,
116 /// Returns `true` if `line` can be ignored. This is the case when it contains
117 /// an annotation that is explicitly ignored.
118 fn should_ignore(line
: &str) -> bool
{
119 // Matches test annotations like `//~ ERROR text`.
120 // This mirrors the regex in src/tools/compiletest/src/runtest.rs, please
121 // update both if either are changed.
122 let re
= Regex
::new("\\s*//(\\[.*\\])?~.*").unwrap();
123 re
.is_match(line
) || ANNOTATIONS_TO_IGNORE
.iter().any(|a
| line
.contains(a
))
126 /// Returns `true` if `line` is allowed to be longer than the normal limit.
127 fn long_line_is_ok(extension
: &str, is_error_code
: bool
, max_columns
: usize, line
: &str) -> bool
{
128 if extension
!= "md" || is_error_code
{
129 if line_is_url(is_error_code
, max_columns
, line
) || should_ignore(line
) {
132 } else if extension
== "md" {
133 // non-error code markdown is allowed to be any length
141 /// By default, tidy always warns against style issues.
144 /// `Ignore(false)` means that an `ignore-tidy-*` directive
145 /// has been provided, but is unnecessary. `Ignore(true)`
146 /// means that it is necessary (i.e. a warning would be
147 /// produced if `ignore-tidy-*` was not present).
151 fn contains_ignore_directive(can_contain
: bool
, contents
: &str, check
: &str) -> Directive
{
153 return Directive
::Deny
;
155 // Update `can_contain` when changing this
156 if contents
.contains(&format
!("// ignore-tidy-{}", check
))
157 || contents
.contains(&format
!("# ignore-tidy-{}", check
))
158 || contents
.contains(&format
!("/* ignore-tidy-{} */", check
))
160 Directive
::Ignore(false)
166 macro_rules
! suppressible_tidy_err
{
167 ($err
:ident
, $skip
:ident
, $msg
:expr
) => {
168 if let Directive
::Deny
= $skip
{
171 $skip
= Directive
::Ignore(true);
176 pub fn is_in(full_path
: &Path
, parent_folder_to_find
: &str, folder_to_find
: &str) -> bool
{
177 if let Some(parent
) = full_path
.parent() {
178 if parent
.file_name().map_or_else(
181 f
.to_string_lossy() == folder_to_find
184 .and_then(|f
| f
.file_name())
185 .map_or_else(|| false, |f
| f
== parent_folder_to_find
)
190 is_in(parent
, parent_folder_to_find
, folder_to_find
)
197 fn skip_markdown_path(path
: &Path
) -> bool
{
198 // These aren't ready for tidy.
199 const SKIP_MD
: &[&str] = &[
200 "src/doc/edition-guide",
201 "src/doc/embedded-book",
204 "src/doc/rust-by-example",
205 "src/doc/rustc-dev-guide",
207 SKIP_MD
.iter().any(|p
| path
.ends_with(p
))
210 fn is_unexplained_ignore(extension
: &str, line
: &str) -> bool
{
211 if !line
.ends_with("```ignore") && !line
.ends_with("```rust,ignore") {
214 if extension
== "md" && line
.trim().starts_with("//") {
215 // Markdown examples may include doc comments with ignore inside a
222 pub fn check(path
: &Path
, bad
: &mut bool
) {
223 fn skip(path
: &Path
) -> bool
{
224 super::filter_dirs(path
) || skip_markdown_path(path
)
226 let problematic_consts_strings
: Vec
<String
> = (PROBLEMATIC_CONSTS
.iter().map(u32::to_string
))
227 .chain(PROBLEMATIC_CONSTS
.iter().map(|v
| format
!("{:x}", v
)))
228 .chain(PROBLEMATIC_CONSTS
.iter().map(|v
| format
!("{:X}", v
)))
230 super::walk(path
, &mut skip
, &mut |entry
, contents
| {
231 let file
= entry
.path();
232 let filename
= file
.file_name().unwrap().to_string_lossy();
233 let extensions
= [".rs", ".py", ".js", ".sh", ".c", ".cpp", ".h", ".md", ".css"];
234 if extensions
.iter().all(|e
| !filename
.ends_with(e
)) || filename
.starts_with(".#") {
238 let is_style_file
= filename
.ends_with(".css");
239 let under_rustfmt
= filename
.ends_with(".rs") &&
240 // This list should ideally be sourced from rustfmt.toml but we don't want to add a toml
242 !file
.ancestors().any(|a
| {
243 a
.ends_with("src/test") ||
244 a
.ends_with("src/doc/book")
247 if is_style_file
&& !is_in(file
, "src", "librustdoc") {
248 // We only check CSS files in rustdoc.
252 if contents
.is_empty() {
253 tidy_error
!(bad
, "{}: empty file", file
.display());
256 let extension
= file
.extension().unwrap().to_string_lossy();
257 let is_error_code
= extension
== "md" && is_in(file
, "src", "error_codes");
259 let max_columns
= if is_error_code { ERROR_CODE_COLS }
else { COLS }
;
261 let can_contain
= contents
.contains("// ignore-tidy-")
262 || contents
.contains("# ignore-tidy-")
263 || contents
.contains("/* ignore-tidy-");
264 // Enable testing ICE's that require specific (untidy)
265 // file formats easily eg. `issue-1234-ignore-tidy.rs`
266 if filename
.contains("ignore-tidy") {
269 let mut skip_cr
= contains_ignore_directive(can_contain
, &contents
, "cr");
270 let mut skip_undocumented_unsafe
=
271 contains_ignore_directive(can_contain
, &contents
, "undocumented-unsafe");
272 let mut skip_tab
= contains_ignore_directive(can_contain
, &contents
, "tab");
273 let mut skip_line_length
= contains_ignore_directive(can_contain
, &contents
, "linelength");
274 let mut skip_file_length
= contains_ignore_directive(can_contain
, &contents
, "filelength");
275 let mut skip_end_whitespace
=
276 contains_ignore_directive(can_contain
, &contents
, "end-whitespace");
277 let mut skip_trailing_newlines
=
278 contains_ignore_directive(can_contain
, &contents
, "trailing-newlines");
279 let mut skip_leading_newlines
=
280 contains_ignore_directive(can_contain
, &contents
, "leading-newlines");
281 let mut skip_copyright
= contains_ignore_directive(can_contain
, &contents
, "copyright");
282 let mut leading_new_lines
= false;
283 let mut trailing_new_lines
= 0;
285 let mut last_safety_comment
= false;
286 for (i
, line
) in contents
.split('
\n'
).enumerate() {
287 let mut err
= |msg
: &str| {
288 tidy_error
!(bad
, "{}:{}: {}", file
.display(), i
+ 1, msg
);
291 && line
.chars().count() > max_columns
292 && !long_line_is_ok(&extension
, is_error_code
, max_columns
, line
)
294 suppressible_tidy_err
!(
297 &format
!("line longer than {} chars", max_columns
)
300 if !is_style_file
&& line
.contains('
\t'
) {
301 suppressible_tidy_err
!(err
, skip_tab
, "tab character");
303 if line
.ends_with(' '
) || line
.ends_with('
\t'
) {
304 suppressible_tidy_err
!(err
, skip_end_whitespace
, "trailing whitespace");
306 if is_style_file
&& line
.starts_with(' '
) {
307 err("CSS files use tabs for indent");
309 if line
.contains('
\r'
) {
310 suppressible_tidy_err
!(err
, skip_cr
, "CR character");
312 if filename
!= "style.rs" {
313 if line
.contains("TODO") {
314 err("TODO is deprecated; use FIXME")
316 if line
.contains("//") && line
.contains(" XXX") {
317 err("XXX is deprecated; use FIXME")
319 for s
in problematic_consts_strings
.iter() {
320 if line
.contains(s
) {
321 err("Don't use magic numbers that spell things (consider 0x12345678)");
325 let is_test
= || file
.components().any(|c
| c
.as_os_str() == "tests");
326 // for now we just check libcore
327 if line
.contains("unsafe {") && !line
.trim().starts_with("//") && !last_safety_comment
{
328 if file
.components().any(|c
| c
.as_os_str() == "core") && !is_test() {
329 suppressible_tidy_err
!(err
, skip_undocumented_unsafe
, "undocumented unsafe");
332 if line
.contains("// SAFETY:") {
333 last_safety_comment
= true;
334 } else if line
.trim().starts_with("//") || line
.trim().is_empty() {
335 // keep previous value
337 last_safety_comment
= false;
339 if (line
.starts_with("// Copyright")
340 || line
.starts_with("# Copyright")
341 || line
.starts_with("Copyright"))
342 && (line
.contains("Rust Developers") || line
.contains("Rust Project Developers"))
344 suppressible_tidy_err
!(
347 "copyright notices attributed to the Rust Project Developers are deprecated"
350 if is_unexplained_ignore(&extension
, line
) {
351 err(UNEXPLAINED_IGNORE_DOCTEST_INFO
);
353 if filename
.ends_with(".cpp") && line
.contains("llvm_unreachable") {
354 err(LLVM_UNREACHABLE_INFO
);
358 leading_new_lines
= true;
360 trailing_new_lines
+= 1;
362 trailing_new_lines
= 0;
365 if !line
.trim().starts_with("//") {
369 if leading_new_lines
{
371 tidy_error
!(bad
, "{}: leading newline", file
.display());
373 suppressible_tidy_err
!(err
, skip_leading_newlines
, "mising leading newline");
375 let mut err
= |msg
: &str| {
376 tidy_error
!(bad
, "{}: {}", file
.display(), msg
);
378 match trailing_new_lines
{
379 0 => suppressible_tidy_err
!(err
, skip_trailing_newlines
, "missing trailing newline"),
381 n
=> suppressible_tidy_err
!(
383 skip_trailing_newlines
,
384 &format
!("too many trailing newlines ({})", n
)
391 "{}: too many lines ({}) (add `// \
392 ignore-tidy-filelength` to the file to suppress this error)",
397 suppressible_tidy_err
!(err
, skip_file_length
, "");
400 if let Directive
::Ignore(false) = skip_cr
{
401 tidy_error
!(bad
, "{}: ignoring CR characters unnecessarily", file
.display());
403 if let Directive
::Ignore(false) = skip_tab
{
404 tidy_error
!(bad
, "{}: ignoring tab characters unnecessarily", file
.display());
406 if let Directive
::Ignore(false) = skip_line_length
{
407 tidy_error
!(bad
, "{}: ignoring line length unnecessarily", file
.display());
409 if let Directive
::Ignore(false) = skip_file_length
{
410 tidy_error
!(bad
, "{}: ignoring file length unnecessarily", file
.display());
412 if let Directive
::Ignore(false) = skip_end_whitespace
{
413 tidy_error
!(bad
, "{}: ignoring trailing whitespace unnecessarily", file
.display());
415 if let Directive
::Ignore(false) = skip_trailing_newlines
{
416 tidy_error
!(bad
, "{}: ignoring trailing newlines unnecessarily", file
.display());
418 if let Directive
::Ignore(false) = skip_leading_newlines
{
419 tidy_error
!(bad
, "{}: ignoring leading newlines unnecessarily", file
.display());
421 if let Directive
::Ignore(false) = skip_copyright
{
422 tidy_error
!(bad
, "{}: ignoring copyright unnecessarily", file
.display());