]>
Commit | Line | Data |
---|---|---|
a7813a04 XL |
1 | //! Tidy check to enforce various stylistic guidelines on the Rust codebase. |
2 | //! | |
3 | //! Example checks are: | |
4 | //! | |
dfeec247 XL |
5 | //! * No lines over 100 characters (in non-Rust files). |
6 | //! * No files with over 3000 lines (in non-Rust files). | |
9fa01778 XL |
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. | |
a7813a04 | 12 | //! |
dfeec247 XL |
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. | |
15 | //! | |
48663c56 XL |
16 | //! A number of these checks can be opted-out of with various directives of the form: |
17 | //! `// ignore-tidy-CHECK-NAME`. | |
a7813a04 | 18 | |
a7813a04 XL |
19 | use std::path::Path; |
20 | ||
e1599b0c | 21 | const ERROR_CODE_COLS: usize = 80; |
a7813a04 | 22 | const COLS: usize = 100; |
a7813a04 | 23 | |
48663c56 XL |
24 | const LINES: usize = 3000; |
25 | ||
041b39d2 XL |
26 | const UNEXPLAINED_IGNORE_DOCTEST_INFO: &str = r#"unexplained "```ignore" doctest; try one: |
27 | ||
28 | * make the test actually pass, by adding necessary imports and declarations, or | |
29 | * use "```text", if the code is not Rust code, or | |
30 | * use "```compile_fail,Ennnn", if the code is expected to fail at compile time, or | |
31 | * use "```should_panic", if the code is expected to fail at run time, or | |
32 | * use "```no_run", if the code should type-check but not necessary linkable/runnable, or | |
33 | * explain it like "```ignore (cannot-test-this-because-xxxx)", if the annotation cannot be avoided. | |
34 | ||
35 | "#; | |
36 | ||
ff7c6d11 XL |
37 | const LLVM_UNREACHABLE_INFO: &str = r"\ |
38 | C++ code used llvm_unreachable, which triggers undefined behavior | |
39 | when executed when assertions are disabled. | |
40 | Use llvm::report_fatal_error for increased robustness."; | |
41 | ||
9fa01778 | 42 | /// Parser states for `line_is_url`. |
48663c56 | 43 | #[derive(Clone, Copy, PartialEq)] |
8bb4bdeb | 44 | #[allow(non_camel_case_types)] |
9fa01778 XL |
45 | enum LIUState { |
46 | EXP_COMMENT_START, | |
47 | EXP_LINK_LABEL_OR_URL, | |
48 | EXP_URL, | |
49 | EXP_END, | |
50 | } | |
8bb4bdeb | 51 | |
9fa01778 | 52 | /// Returns `true` if `line` appears to be a line comment containing an URL, |
8bb4bdeb XL |
53 | /// possibly with a Markdown link label in front, and nothing else. |
54 | /// The Markdown link label, if present, may not contain whitespace. | |
55 | /// Lines of this form are allowed to be overlength, because Markdown | |
56 | /// offers no way to split a line in the middle of a URL, and the lengths | |
57 | /// of URLs to external references are beyond our control. | |
e1599b0c XL |
58 | fn line_is_url(columns: usize, line: &str) -> bool { |
59 | // more basic check for error_codes.rs, to avoid complexity in implementing two state machines | |
60 | if columns == ERROR_CODE_COLS { | |
dfeec247 | 61 | return line.starts_with('[') && line.contains("]:") && line.contains("http"); |
e1599b0c XL |
62 | } |
63 | ||
8bb4bdeb XL |
64 | use self::LIUState::*; |
65 | let mut state: LIUState = EXP_COMMENT_START; | |
48663c56 | 66 | let is_url = |w: &str| w.starts_with("http://") || w.starts_with("https://"); |
8bb4bdeb XL |
67 | |
68 | for tok in line.split_whitespace() { | |
69 | match (state, tok) { | |
dfeec247 XL |
70 | (EXP_COMMENT_START, "//") | (EXP_COMMENT_START, "///") | (EXP_COMMENT_START, "//!") => { |
71 | state = EXP_LINK_LABEL_OR_URL | |
72 | } | |
8bb4bdeb XL |
73 | |
74 | (EXP_LINK_LABEL_OR_URL, w) | |
dfeec247 XL |
75 | if w.len() >= 4 && w.starts_with('[') && w.ends_with("]:") => |
76 | { | |
77 | state = EXP_URL | |
78 | } | |
8bb4bdeb | 79 | |
dfeec247 | 80 | (EXP_LINK_LABEL_OR_URL, w) if is_url(w) => state = EXP_END, |
8bb4bdeb | 81 | |
dfeec247 | 82 | (EXP_URL, w) if is_url(w) || w.starts_with("../") => state = EXP_END, |
48663c56 | 83 | |
dfeec247 | 84 | (_, w) if w.len() > columns && is_url(w) => state = EXP_END, |
8bb4bdeb | 85 | |
48663c56 | 86 | (_, _) => {} |
8bb4bdeb XL |
87 | } |
88 | } | |
89 | ||
90 | state == EXP_END | |
91 | } | |
92 | ||
9fa01778 | 93 | /// Returns `true` if `line` is allowed to be longer than the normal limit. |
8bb4bdeb XL |
94 | /// Currently there is only one exception, for long URLs, but more |
95 | /// may be added in the future. | |
e1599b0c XL |
96 | fn long_line_is_ok(max_columns: usize, line: &str) -> bool { |
97 | if line_is_url(max_columns, line) { | |
8bb4bdeb XL |
98 | return true; |
99 | } | |
100 | ||
101 | false | |
102 | } | |
103 | ||
48663c56 XL |
104 | enum Directive { |
105 | /// By default, tidy always warns against style issues. | |
106 | Deny, | |
107 | ||
108 | /// `Ignore(false)` means that an `ignore-tidy-*` directive | |
109 | /// has been provided, but is unnecessary. `Ignore(true)` | |
110 | /// means that it is necessary (i.e. a warning would be | |
111 | /// produced if `ignore-tidy-*` was not present). | |
112 | Ignore(bool), | |
113 | } | |
114 | ||
dc9dc135 XL |
115 | fn contains_ignore_directive(can_contain: bool, contents: &str, check: &str) -> Directive { |
116 | if !can_contain { | |
117 | return Directive::Deny; | |
118 | } | |
119 | // Update `can_contain` when changing this | |
dfeec247 XL |
120 | if contents.contains(&format!("// ignore-tidy-{}", check)) |
121 | || contents.contains(&format!("# ignore-tidy-{}", check)) | |
3dfed10e | 122 | || contents.contains(&format!("/* ignore-tidy-{} */", check)) |
dfeec247 | 123 | { |
48663c56 XL |
124 | Directive::Ignore(false) |
125 | } else { | |
126 | Directive::Deny | |
127 | } | |
128 | } | |
129 | ||
130 | macro_rules! suppressible_tidy_err { | |
131 | ($err:ident, $skip:ident, $msg:expr) => { | |
132 | if let Directive::Deny = $skip { | |
133 | $err($msg); | |
134 | } else { | |
135 | $skip = Directive::Ignore(true); | |
136 | } | |
137 | }; | |
138 | } | |
139 | ||
3dfed10e XL |
140 | pub fn is_in(full_path: &Path, parent_folder_to_find: &str, folder_to_find: &str) -> bool { |
141 | if let Some(parent) = full_path.parent() { | |
142 | if parent.file_name().map_or_else( | |
143 | || false, | |
144 | |f| { | |
145 | f.to_string_lossy() == folder_to_find | |
146 | && parent | |
147 | .parent() | |
148 | .and_then(|f| f.file_name()) | |
149 | .map_or_else(|| false, |f| f == parent_folder_to_find) | |
150 | }, | |
151 | ) { | |
152 | true | |
153 | } else { | |
154 | is_in(parent, parent_folder_to_find, folder_to_find) | |
155 | } | |
156 | } else { | |
157 | false | |
158 | } | |
159 | } | |
160 | ||
a7813a04 | 161 | pub fn check(path: &Path, bad: &mut bool) { |
dc9dc135 XL |
162 | super::walk(path, &mut super::filter_dirs, &mut |entry, contents| { |
163 | let file = entry.path(); | |
a7813a04 | 164 | let filename = file.file_name().unwrap().to_string_lossy(); |
3dfed10e | 165 | let extensions = [".rs", ".py", ".js", ".sh", ".c", ".cpp", ".h", ".md", ".css"]; |
dfeec247 XL |
166 | if extensions.iter().all(|e| !filename.ends_with(e)) || filename.starts_with(".#") { |
167 | return; | |
a7813a04 | 168 | } |
a7813a04 | 169 | |
3dfed10e | 170 | let is_style_file = filename.ends_with(".css"); |
dfeec247 XL |
171 | let under_rustfmt = filename.ends_with(".rs") && |
172 | // This list should ideally be sourced from rustfmt.toml but we don't want to add a toml | |
173 | // parser to tidy. | |
174 | !file.ancestors().any(|a| { | |
175 | a.ends_with("src/test") || | |
dfeec247 XL |
176 | a.ends_with("src/doc/book") |
177 | }); | |
178 | ||
179 | if filename.ends_with(".md") | |
180 | && file.parent().unwrap().file_name().unwrap().to_string_lossy() != "error_codes" | |
181 | { | |
60c5eb7d XL |
182 | // We don't want to check all ".md" files (almost of of them aren't compliant |
183 | // currently), just the long error code explanation ones. | |
184 | return; | |
185 | } | |
3dfed10e XL |
186 | if is_style_file && !is_in(file, "src", "librustdoc") { |
187 | // We only check CSS files in rustdoc. | |
188 | return; | |
189 | } | |
60c5eb7d | 190 | |
7cac9316 XL |
191 | if contents.is_empty() { |
192 | tidy_error!(bad, "{}: empty file", file.display()); | |
193 | } | |
194 | ||
60c5eb7d | 195 | let max_columns = if filename == "error_codes.rs" || filename.ends_with(".md") { |
e1599b0c XL |
196 | ERROR_CODE_COLS |
197 | } else { | |
198 | COLS | |
199 | }; | |
200 | ||
3dfed10e XL |
201 | let can_contain = contents.contains("// ignore-tidy-") |
202 | || contents.contains("# ignore-tidy-") | |
203 | || contents.contains("/* ignore-tidy-"); | |
f9f354fc XL |
204 | // Enable testing ICE's that require specific (untidy) |
205 | // file formats easily eg. `issue-1234-ignore-tidy.rs` | |
206 | if filename.contains("ignore-tidy") { | |
207 | return; | |
208 | } | |
dc9dc135 | 209 | let mut skip_cr = contains_ignore_directive(can_contain, &contents, "cr"); |
60c5eb7d XL |
210 | let mut skip_undocumented_unsafe = |
211 | contains_ignore_directive(can_contain, &contents, "undocumented-unsafe"); | |
dc9dc135 XL |
212 | let mut skip_tab = contains_ignore_directive(can_contain, &contents, "tab"); |
213 | let mut skip_line_length = contains_ignore_directive(can_contain, &contents, "linelength"); | |
214 | let mut skip_file_length = contains_ignore_directive(can_contain, &contents, "filelength"); | |
215 | let mut skip_end_whitespace = | |
216 | contains_ignore_directive(can_contain, &contents, "end-whitespace"); | |
416331ca XL |
217 | let mut skip_trailing_newlines = |
218 | contains_ignore_directive(can_contain, &contents, "trailing-newlines"); | |
dc9dc135 | 219 | let mut skip_copyright = contains_ignore_directive(can_contain, &contents, "copyright"); |
48663c56 | 220 | let mut leading_new_lines = false; |
ff7c6d11 | 221 | let mut trailing_new_lines = 0; |
48663c56 | 222 | let mut lines = 0; |
60c5eb7d | 223 | let mut last_safety_comment = false; |
8faf50e0 | 224 | for (i, line) in contents.split('\n').enumerate() { |
a7813a04 | 225 | let mut err = |msg: &str| { |
cc61c64b | 226 | tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg); |
a7813a04 | 227 | }; |
dfeec247 XL |
228 | if !under_rustfmt |
229 | && line.chars().count() > max_columns | |
230 | && !long_line_is_ok(max_columns, line) | |
231 | { | |
48663c56 XL |
232 | suppressible_tidy_err!( |
233 | err, | |
234 | skip_line_length, | |
e1599b0c | 235 | &format!("line longer than {} chars", max_columns) |
48663c56 | 236 | ); |
a7813a04 | 237 | } |
3dfed10e | 238 | if !is_style_file && line.contains('\t') { |
48663c56 | 239 | suppressible_tidy_err!(err, skip_tab, "tab character"); |
a7813a04 | 240 | } |
48663c56 XL |
241 | if line.ends_with(' ') || line.ends_with('\t') { |
242 | suppressible_tidy_err!(err, skip_end_whitespace, "trailing whitespace"); | |
a7813a04 | 243 | } |
3dfed10e XL |
244 | if is_style_file && line.starts_with(' ') { |
245 | err("CSS files use tabs for indent"); | |
246 | } | |
48663c56 XL |
247 | if line.contains('\r') { |
248 | suppressible_tidy_err!(err, skip_cr, "CR character"); | |
a7813a04 XL |
249 | } |
250 | if filename != "style.rs" { | |
251 | if line.contains("TODO") { | |
252 | err("TODO is deprecated; use FIXME") | |
253 | } | |
254 | if line.contains("//") && line.contains(" XXX") { | |
255 | err("XXX is deprecated; use FIXME") | |
256 | } | |
257 | } | |
60c5eb7d XL |
258 | let is_test = || file.components().any(|c| c.as_os_str() == "tests"); |
259 | // for now we just check libcore | |
260 | if line.contains("unsafe {") && !line.trim().starts_with("//") && !last_safety_comment { | |
3dfed10e | 261 | if file.components().any(|c| c.as_os_str() == "core") && !is_test() { |
60c5eb7d XL |
262 | suppressible_tidy_err!(err, skip_undocumented_unsafe, "undocumented unsafe"); |
263 | } | |
264 | } | |
3dfed10e | 265 | if line.contains("// SAFETY:") || line.contains("// Safety:") { |
60c5eb7d XL |
266 | last_safety_comment = true; |
267 | } else if line.trim().starts_with("//") || line.trim().is_empty() { | |
268 | // keep previous value | |
269 | } else { | |
270 | last_safety_comment = false; | |
271 | } | |
dfeec247 XL |
272 | if (line.starts_with("// Copyright") |
273 | || line.starts_with("# Copyright") | |
274 | || line.starts_with("Copyright")) | |
275 | && (line.contains("Rust Developers") || line.contains("Rust Project Developers")) | |
276 | { | |
48663c56 XL |
277 | suppressible_tidy_err!( |
278 | err, | |
279 | skip_copyright, | |
280 | "copyright notices attributed to the Rust Project Developers are deprecated" | |
281 | ); | |
9fa01778 | 282 | } |
041b39d2 XL |
283 | if line.ends_with("```ignore") || line.ends_with("```rust,ignore") { |
284 | err(UNEXPLAINED_IGNORE_DOCTEST_INFO); | |
285 | } | |
ff7c6d11 XL |
286 | if filename.ends_with(".cpp") && line.contains("llvm_unreachable") { |
287 | err(LLVM_UNREACHABLE_INFO); | |
288 | } | |
289 | if line.is_empty() { | |
48663c56 XL |
290 | if i == 0 { |
291 | leading_new_lines = true; | |
292 | } | |
ff7c6d11 XL |
293 | trailing_new_lines += 1; |
294 | } else { | |
295 | trailing_new_lines = 0; | |
296 | } | |
48663c56 XL |
297 | lines = i; |
298 | } | |
299 | if leading_new_lines { | |
300 | tidy_error!(bad, "{}: leading newline", file.display()); | |
a7813a04 | 301 | } |
416331ca XL |
302 | let mut err = |msg: &str| { |
303 | tidy_error!(bad, "{}: {}", file.display(), msg); | |
304 | }; | |
ff7c6d11 | 305 | match trailing_new_lines { |
416331ca | 306 | 0 => suppressible_tidy_err!(err, skip_trailing_newlines, "missing trailing newline"), |
48663c56 | 307 | 1 => {} |
416331ca XL |
308 | n => suppressible_tidy_err!( |
309 | err, | |
310 | skip_trailing_newlines, | |
311 | &format!("too many trailing newlines ({})", n) | |
312 | ), | |
ff7c6d11 | 313 | }; |
48663c56 XL |
314 | if lines > LINES { |
315 | let mut err = |_| { | |
316 | tidy_error!( | |
317 | bad, | |
318 | "{}: too many lines ({}) (add `// \ | |
319 | ignore-tidy-filelength` to the file to suppress this error)", | |
320 | file.display(), | |
321 | lines | |
322 | ); | |
323 | }; | |
324 | suppressible_tidy_err!(err, skip_file_length, ""); | |
325 | } | |
326 | ||
327 | if let Directive::Ignore(false) = skip_cr { | |
328 | tidy_error!(bad, "{}: ignoring CR characters unnecessarily", file.display()); | |
329 | } | |
330 | if let Directive::Ignore(false) = skip_tab { | |
331 | tidy_error!(bad, "{}: ignoring tab characters unnecessarily", file.display()); | |
332 | } | |
333 | if let Directive::Ignore(false) = skip_line_length { | |
334 | tidy_error!(bad, "{}: ignoring line length unnecessarily", file.display()); | |
335 | } | |
336 | if let Directive::Ignore(false) = skip_file_length { | |
337 | tidy_error!(bad, "{}: ignoring file length unnecessarily", file.display()); | |
338 | } | |
339 | if let Directive::Ignore(false) = skip_end_whitespace { | |
340 | tidy_error!(bad, "{}: ignoring trailing whitespace unnecessarily", file.display()); | |
341 | } | |
416331ca XL |
342 | if let Directive::Ignore(false) = skip_trailing_newlines { |
343 | tidy_error!(bad, "{}: ignoring trailing newlines unnecessarily", file.display()); | |
344 | } | |
48663c56 XL |
345 | if let Directive::Ignore(false) = skip_copyright { |
346 | tidy_error!(bad, "{}: ignoring copyright unnecessarily", file.display()); | |
347 | } | |
a7813a04 XL |
348 | }) |
349 | } |