]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/style.rs
Add a tidy test for line count
[rust.git] / src / tools / tidy / src / style.rs
1 //! Tidy check to enforce various stylistic guidelines on the Rust codebase.
2 //!
3 //! Example checks are:
4 //!
5 //! * No lines over 100 characters.
6 //! * No files with over 3000 lines.
7 //! * No tabs.
8 //! * No trailing whitespace.
9 //! * No CR characters.
10 //! * No `TODO` or `XXX` directives.
11 //! * No unexplained ` ```ignore ` or ` ```rust,ignore ` doc tests.
12 //!
13 //! A number of these checks can be opted-out of with various directives of the form:
14 //! `// ignore-tidy-CHECK-NAME`.
15
16 use std::fs::File;
17 use std::io::prelude::*;
18 use std::path::Path;
19
20 const COLS: usize = 100;
21
22 const LINES: usize = 3000;
23
24 const UNEXPLAINED_IGNORE_DOCTEST_INFO: &str = r#"unexplained "```ignore" doctest; try one:
25
26 * make the test actually pass, by adding necessary imports and declarations, or
27 * use "```text", if the code is not Rust code, or
28 * use "```compile_fail,Ennnn", if the code is expected to fail at compile time, or
29 * use "```should_panic", if the code is expected to fail at run time, or
30 * use "```no_run", if the code should type-check but not necessary linkable/runnable, or
31 * explain it like "```ignore (cannot-test-this-because-xxxx)", if the annotation cannot be avoided.
32
33 "#;
34
35 const LLVM_UNREACHABLE_INFO: &str = r"\
36 C++ code used llvm_unreachable, which triggers undefined behavior
37 when executed when assertions are disabled.
38 Use llvm::report_fatal_error for increased robustness.";
39
40 /// Parser states for `line_is_url`.
41 #[derive(PartialEq)]
42 #[allow(non_camel_case_types)]
43 enum LIUState {
44     EXP_COMMENT_START,
45     EXP_LINK_LABEL_OR_URL,
46     EXP_URL,
47     EXP_END,
48 }
49
50 /// Returns `true` if `line` appears to be a line comment containing an URL,
51 /// possibly with a Markdown link label in front, and nothing else.
52 /// The Markdown link label, if present, may not contain whitespace.
53 /// Lines of this form are allowed to be overlength, because Markdown
54 /// offers no way to split a line in the middle of a URL, and the lengths
55 /// of URLs to external references are beyond our control.
56 fn line_is_url(line: &str) -> bool {
57     use self::LIUState::*;
58     let mut state: LIUState = EXP_COMMENT_START;
59
60     for tok in line.split_whitespace() {
61         match (state, tok) {
62             (EXP_COMMENT_START, "//") => state = EXP_LINK_LABEL_OR_URL,
63             (EXP_COMMENT_START, "///") => state = EXP_LINK_LABEL_OR_URL,
64             (EXP_COMMENT_START, "//!") => state = EXP_LINK_LABEL_OR_URL,
65
66             (EXP_LINK_LABEL_OR_URL, w)
67                 if w.len() >= 4 && w.starts_with('[') && w.ends_with("]:")
68                 => state = EXP_URL,
69
70             (EXP_LINK_LABEL_OR_URL, w)
71                 if w.starts_with("http://") || w.starts_with("https://")
72                 => state = EXP_END,
73
74             (EXP_URL, w)
75                 if w.starts_with("http://") || w.starts_with("https://") || w.starts_with("../")
76                 => state = EXP_END,
77
78             (_, _) => return false,
79         }
80     }
81
82     state == EXP_END
83 }
84
85 /// Returns `true` if `line` is allowed to be longer than the normal limit.
86 /// Currently there is only one exception, for long URLs, but more
87 /// may be added in the future.
88 fn long_line_is_ok(line: &str) -> bool {
89     if line_is_url(line) {
90         return true;
91     }
92
93     false
94 }
95
96 enum Directive {
97     /// By default, tidy always warns against style issues.
98     Deny,
99
100     /// `Ignore(false)` means that an `ignore-tidy-*` directive
101     /// has been provided, but is unnecessary. `Ignore(true)`
102     /// means that it is necessary (i.e. a warning would be
103     /// produced if `ignore-tidy-*` was not present).
104     Ignore(bool),
105 }
106
107 fn contains_ignore_directive(contents: &String, check: &str) -> Directive {
108     if contents.contains(&format!("// ignore-tidy-{}", check)) ||
109         contents.contains(&format!("# ignore-tidy-{}", check)) {
110         Directive::Ignore(false)
111     } else {
112         Directive::Deny
113     }
114 }
115
116 macro_rules! suppressible_tidy_err {
117     ($err:ident, $skip:ident, $msg:expr) => {
118         if let Directive::Deny = $skip {
119             $err($msg);
120         } else {
121             $skip = Directive::Ignore(true);
122         }
123     };
124 }
125
126 pub fn check(path: &Path, bad: &mut bool) {
127     let mut contents = String::new();
128     super::walk(path, &mut super::filter_dirs, &mut |file| {
129         let filename = file.file_name().unwrap().to_string_lossy();
130         let extensions = [".rs", ".py", ".js", ".sh", ".c", ".cpp", ".h"];
131         if extensions.iter().all(|e| !filename.ends_with(e)) ||
132            filename.starts_with(".#") {
133             return
134         }
135
136         contents.truncate(0);
137         t!(t!(File::open(file), file).read_to_string(&mut contents));
138
139         if contents.is_empty() {
140             tidy_error!(bad, "{}: empty file", file.display());
141         }
142
143         let mut skip_cr = contains_ignore_directive(&contents, "cr");
144         let mut skip_tab = contains_ignore_directive(&contents, "tab");
145         let mut skip_line_length = contains_ignore_directive(&contents, "linelength");
146         let mut skip_file_length = contains_ignore_directive(&contents, "filelength");
147         let mut skip_end_whitespace = contains_ignore_directive(&contents, "end-whitespace");
148         let mut skip_copyright = contains_ignore_directive(&contents, "copyright");
149         let mut leading_new_lines = false;
150         let mut trailing_new_lines = 0;
151         let mut lines = 0;
152         for (i, line) in contents.split('\n').enumerate() {
153             let mut err = |msg: &str| {
154                 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
155             };
156             if line.chars().count() > COLS && !long_line_is_ok(line) {
157                 suppressible_tidy_err!(
158                     err,
159                     skip_line_length,
160                     &format!("line longer than {} chars", COLS)
161                 );
162             }
163             if line.contains('\t') {
164                 suppressible_tidy_err!(err, skip_tab, "tab character");
165             }
166             if line.ends_with(' ') || line.ends_with('\t') {
167                 suppressible_tidy_err!(err, skip_end_whitespace, "trailing whitespace");
168             }
169             if line.contains('\r') {
170                 suppressible_tidy_err!(err, skip_cr, "CR character");
171             }
172             if filename != "style.rs" {
173                 if line.contains("TODO") {
174                     err("TODO is deprecated; use FIXME")
175                 }
176                 if line.contains("//") && line.contains(" XXX") {
177                     err("XXX is deprecated; use FIXME")
178                 }
179             }
180             if (line.starts_with("// Copyright") ||
181                 line.starts_with("# Copyright") ||
182                 line.starts_with("Copyright"))
183                 && (line.contains("Rust Developers") ||
184                     line.contains("Rust Project Developers")) {
185                 suppressible_tidy_err!(
186                     err,
187                     skip_copyright,
188                     "copyright notices attributed to the Rust Project Developers are deprecated"
189                 );
190             }
191             if line.ends_with("```ignore") || line.ends_with("```rust,ignore") {
192                 err(UNEXPLAINED_IGNORE_DOCTEST_INFO);
193             }
194             if filename.ends_with(".cpp") && line.contains("llvm_unreachable") {
195                 err(LLVM_UNREACHABLE_INFO);
196             }
197             if line.is_empty() {
198                 if i == 0 {
199                     leading_new_lines = true;
200                 }
201                 trailing_new_lines += 1;
202             } else {
203                 trailing_new_lines = 0;
204             }
205             lines = i;
206         }
207         if leading_new_lines {
208             tidy_error!(bad, "{}: leading newline", file.display());
209         }
210         match trailing_new_lines {
211             0 => tidy_error!(bad, "{}: missing trailing newline", file.display()),
212             1 => {}
213             n => tidy_error!(bad, "{}: too many trailing newlines ({})", file.display(), n),
214         };
215         if !skip_file_length && lines > LINES {
216             tidy_error!(bad, "{}: too many lines ({})", file.display(), lines);
217         }
218
219         if let Directive::Ignore(false) = skip_cr {
220             tidy_error!(bad, "{}: ignoring CR characters unnecessarily", file.display());
221         }
222         if let Directive::Ignore(false) = skip_tab {
223             tidy_error!(bad, "{}: ignoring tab characters unnecessarily", file.display());
224         }
225         if let Directive::Ignore(false) = skip_line_length {
226             tidy_error!(bad, "{}: ignoring line length unnecessarily", file.display());
227         }
228         if let Directive::Ignore(false) = skip_file_length {
229             tidy_error!(bad, "{}: ignoring file length unnecessarily", file.display());
230         }
231         if let Directive::Ignore(false) = skip_end_whitespace {
232             tidy_error!(bad, "{}: ignoring trailing whitespace unnecessarily", file.display());
233         }
234         if let Directive::Ignore(false) = skip_copyright {
235             tidy_error!(bad, "{}: ignoring copyright unnecessarily", file.display());
236         }
237     })
238 }