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