]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/style.rs
Rollup merge of #42006 - jseyfried:fix_include_regression, r=nrc
[rust.git] / src / tools / tidy / src / style.rs
1 // Copyright 2016 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 //! Tidy check to enforce various stylistic guidelines on the Rust codebase.
12 //!
13 //! Example checks are:
14 //!
15 //! * No lines over 100 characters
16 //! * No tabs
17 //! * No trailing whitespace
18 //! * No CR characters
19 //! * No `TODO` or `XXX` directives
20 //! * A valid license header is at the top
21 //!
22 //! A number of these checks can be opted-out of with various directives like
23 //! `// ignore-tidy-linelength`.
24
25 use std::fs::File;
26 use std::io::prelude::*;
27 use std::path::Path;
28
29 const COLS: usize = 100;
30 const LICENSE: &'static str = "\
31 Copyright <year> The Rust Project Developers. See the COPYRIGHT
32 file at the top-level directory of this distribution and at
33 http://rust-lang.org/COPYRIGHT.
34
35 Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
36 http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
37 <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
38 option. This file may not be copied, modified, or distributed
39 except according to those terms.";
40
41 /// Parser states for line_is_url.
42 #[derive(PartialEq)]
43 #[allow(non_camel_case_types)]
44 enum LIUState { EXP_COMMENT_START,
45                 EXP_LINK_LABEL_OR_URL,
46                 EXP_URL,
47                 EXP_END }
48
49 /// True if LINE appears to be a line comment containing an URL,
50 /// possibly with a Markdown link label in front, and nothing else.
51 /// The Markdown link label, if present, may not contain whitespace.
52 /// Lines of this form are allowed to be overlength, because Markdown
53 /// offers no way to split a line in the middle of a URL, and the lengths
54 /// of URLs to external references are beyond our control.
55 fn line_is_url(line: &str) -> bool {
56     use self::LIUState::*;
57     let mut state: LIUState = EXP_COMMENT_START;
58
59     for tok in line.split_whitespace() {
60         match (state, tok) {
61             (EXP_COMMENT_START, "//") => state = EXP_LINK_LABEL_OR_URL,
62             (EXP_COMMENT_START, "///") => state = EXP_LINK_LABEL_OR_URL,
63             (EXP_COMMENT_START, "//!") => state = EXP_LINK_LABEL_OR_URL,
64
65             (EXP_LINK_LABEL_OR_URL, w)
66                 if w.len() >= 4 && w.starts_with("[") && w.ends_with("]:")
67                 => state = EXP_URL,
68
69             (EXP_LINK_LABEL_OR_URL, w)
70                 if w.starts_with("http://") || w.starts_with("https://")
71                 => state = EXP_END,
72
73             (EXP_URL, w)
74                 if w.starts_with("http://") || w.starts_with("https://")
75                 => state = EXP_END,
76
77             (_, _) => return false,
78         }
79     }
80
81     state == EXP_END
82 }
83
84 /// True if LINE is allowed to be longer than the normal limit.
85 /// Currently there is only one exception, for long URLs, but more
86 /// may be added in the future.
87 fn long_line_is_ok(line: &str) -> bool {
88     if line_is_url(line) {
89         return true;
90     }
91
92     false
93 }
94
95 pub fn check(path: &Path, bad: &mut bool) {
96     let mut contents = String::new();
97     super::walk(path, &mut super::filter_dirs, &mut |file| {
98         let filename = file.file_name().unwrap().to_string_lossy();
99         let extensions = [".rs", ".py", ".js", ".sh", ".c", ".h"];
100         if extensions.iter().all(|e| !filename.ends_with(e)) ||
101            filename.starts_with(".#") {
102             return
103         }
104         if filename == "miniz.c" {
105             return
106         }
107
108         contents.truncate(0);
109         t!(t!(File::open(file), file).read_to_string(&mut contents));
110
111         if contents.is_empty() {
112             tidy_error!(bad, "{}: empty file", file.display());
113         }
114
115         let skip_cr = contents.contains("ignore-tidy-cr");
116         let skip_tab = contents.contains("ignore-tidy-tab");
117         let skip_length = contents.contains("ignore-tidy-linelength");
118         let skip_end_whitespace = contents.contains("ignore-tidy-end-whitespace");
119         for (i, line) in contents.split("\n").enumerate() {
120             let mut err = |msg: &str| {
121                 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
122             };
123             if !skip_length && line.chars().count() > COLS
124                 && !long_line_is_ok(line) {
125                     err(&format!("line longer than {} chars", COLS));
126             }
127             if line.contains("\t") && !skip_tab {
128                 err("tab character");
129             }
130             if !skip_end_whitespace && (line.ends_with(" ") || line.ends_with("\t")) {
131                 err("trailing whitespace");
132             }
133             if line.contains("\r") && !skip_cr {
134                 err("CR character");
135             }
136             if filename != "style.rs" {
137                 if line.contains("TODO") {
138                     err("TODO is deprecated; use FIXME")
139                 }
140                 if line.contains("//") && line.contains(" XXX") {
141                     err("XXX is deprecated; use FIXME")
142                 }
143             }
144         }
145         if !licenseck(file, &contents) {
146             tidy_error!(bad, "{}: incorrect license", file.display());
147         }
148     })
149 }
150
151 fn licenseck(file: &Path, contents: &str) -> bool {
152     if contents.contains("ignore-license") {
153         return true
154     }
155     let exceptions = [
156         "libstd/sync/mpsc/mpsc_queue.rs",
157         "libstd/sync/mpsc/spsc_queue.rs",
158     ];
159     if exceptions.iter().any(|f| file.ends_with(f)) {
160         return true
161     }
162
163     // Skip the BOM if it's there
164     let bom = "\u{feff}";
165     let contents = if contents.starts_with(bom) {&contents[3..]} else {contents};
166
167     // See if the license shows up in the first 100 lines
168     let lines = contents.lines().take(100).collect::<Vec<_>>();
169     lines.windows(LICENSE.lines().count()).any(|window| {
170         let offset = if window.iter().all(|w| w.starts_with("//")) {
171             2
172         } else if window.iter().all(|w| w.starts_with("#")) {
173             1
174         } else {
175             return false
176         };
177         window.iter().map(|a| a[offset..].trim())
178               .zip(LICENSE.lines()).all(|(a, b)| {
179             a == b || match b.find("<year>") {
180                 Some(i) => a.starts_with(&b[..i]) && a.ends_with(&b[i+6..]),
181                 None => false,
182             }
183         })
184     })
185
186 }