]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/features.rs
b0bd5ba8dbdbdabef25f2f7faa9426eac2377a5d
[rust.git] / src / tools / tidy / src / features.rs
1 // Copyright 2015 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 ensure that unstable features are all in order
12 //!
13 //! This check will ensure properties like:
14 //!
15 //! * All stability attributes look reasonably well formed
16 //! * The set of library features is disjoint from the set of language features
17 //! * Library features have at most one stability level
18 //! * Library features have at most one `since` value
19 //! * All unstable lang features have tests to ensure they are actually unstable
20
21 use std::collections::HashMap;
22 use std::fmt;
23 use std::fs::{self, File};
24 use std::io::prelude::*;
25 use std::path::Path;
26
27 #[derive(Debug, PartialEq, Clone)]
28 pub enum Status {
29     Stable,
30     Removed,
31     Unstable,
32 }
33
34 impl fmt::Display for Status {
35     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36         let as_str = match *self {
37             Status::Stable => "stable",
38             Status::Unstable => "unstable",
39             Status::Removed => "removed",
40         };
41         fmt::Display::fmt(as_str, f)
42     }
43 }
44
45 #[derive(Debug, Clone)]
46 pub struct Feature {
47     pub level: Status,
48     pub since: String,
49     pub has_gate_test: bool,
50     pub tracking_issue: Option<u32>,
51 }
52
53 pub type Features = HashMap<String, Feature>;
54
55 pub fn check(path: &Path, bad: &mut bool, quiet: bool) {
56     let mut features = collect_lang_features(path, bad);
57     assert!(!features.is_empty());
58
59     let lib_features = get_and_check_lib_features(path, bad, &features);
60     assert!(!lib_features.is_empty());
61
62     let mut contents = String::new();
63
64     super::walk_many(&[&path.join("test/ui-fulldeps"),
65                        &path.join("test/ui"),
66                        &path.join("test/compile-fail"),
67                        &path.join("test/compile-fail-fulldeps"),
68                        &path.join("test/parse-fail"),
69                        &path.join("test/ui"),],
70                      &mut |path| super::filter_dirs(path),
71                      &mut |file| {
72         let filename = file.file_name().unwrap().to_string_lossy();
73         if !filename.ends_with(".rs") || filename == "features.rs" ||
74            filename == "diagnostic_list.rs" {
75             return;
76         }
77
78         let filen_underscore = filename.replace('-',"_").replace(".rs","");
79         let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
80
81         contents.truncate(0);
82         t!(t!(File::open(&file), &file).read_to_string(&mut contents));
83
84         for (i, line) in contents.lines().enumerate() {
85             let mut err = |msg: &str| {
86                 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
87             };
88
89             let gate_test_str = "gate-test-";
90
91             let feature_name = match line.find(gate_test_str) {
92                 Some(i) => {
93                     line[i+gate_test_str.len()..].splitn(2, ' ').next().unwrap()
94                 },
95                 None => continue,
96             };
97             match features.get_mut(feature_name) {
98                 Some(f) => {
99                     if filename_is_gate_test {
100                         err(&format!("The file is already marked as gate test \
101                                       through its name, no need for a \
102                                       'gate-test-{}' comment",
103                                      feature_name));
104                     }
105                     f.has_gate_test = true;
106                 }
107                 None => {
108                     err(&format!("gate-test test found referencing a nonexistent feature '{}'",
109                                  feature_name));
110                 }
111             }
112         }
113     });
114
115     // Only check the number of lang features.
116     // Obligatory testing for library features is dumb.
117     let gate_untested = features.iter()
118                                 .filter(|&(_, f)| f.level == Status::Unstable)
119                                 .filter(|&(_, f)| !f.has_gate_test)
120                                 .collect::<Vec<_>>();
121
122     for &(name, _) in gate_untested.iter() {
123         println!("Expected a gate test for the feature '{}'.", name);
124         println!("Hint: create a failing test file named 'feature-gate-{}.rs'\
125                 \n      in the 'ui' test suite, with its failures due to\
126                 \n      missing usage of #![feature({})].", name, name);
127         println!("Hint: If you already have such a test and don't want to rename it,\
128                 \n      you can also add a // gate-test-{} line to the test file.",
129                  name);
130     }
131
132     if !gate_untested.is_empty() {
133         tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
134     }
135
136     if *bad {
137         return;
138     }
139     if quiet {
140         println!("* {} features", features.len());
141         return;
142     }
143
144     let mut lines = Vec::new();
145     for (name, feature) in features.iter() {
146         lines.push(format!("{:<32} {:<8} {:<12} {:<8}",
147                            name,
148                            "lang",
149                            feature.level,
150                            feature.since));
151     }
152     for (name, feature) in lib_features {
153         lines.push(format!("{:<32} {:<8} {:<12} {:<8}",
154                            name,
155                            "lib",
156                            feature.level,
157                            feature.since));
158     }
159
160     lines.sort();
161     for line in lines {
162         println!("* {}", line);
163     }
164 }
165
166 fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
167     line.find(attr)
168         .and_then(|i| line[i..].find('"').map(|j| i + j + 1))
169         .and_then(|i| line[i..].find('"').map(|j| (i, i + j)))
170         .map(|(i, j)| &line[i..j])
171 }
172
173 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
174     if filen_underscore.starts_with("feature_gate") {
175         for (n, f) in features.iter_mut() {
176             if filen_underscore == format!("feature_gate_{}", n) {
177                 f.has_gate_test = true;
178                 return true;
179             }
180         }
181     }
182     return false;
183 }
184
185 pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
186     let contents = t!(fs::read_to_string(base_src_path.join("libsyntax/feature_gate.rs")));
187
188     // we allow rustc-internal features to omit a tracking issue.
189     // these features must be marked with `// rustc internal` in its own group.
190     let mut next_feature_is_rustc_internal = false;
191
192     contents.lines().zip(1..)
193         .filter_map(|(line, line_number)| {
194             let line = line.trim();
195             if line.starts_with("// rustc internal") {
196                 next_feature_is_rustc_internal = true;
197                 return None;
198             } else if line.is_empty() {
199                 next_feature_is_rustc_internal = false;
200                 return None;
201             }
202
203             let mut parts = line.split(',');
204             let level = match parts.next().map(|l| l.trim().trim_left_matches('(')) {
205                 Some("active") => Status::Unstable,
206                 Some("removed") => Status::Removed,
207                 Some("accepted") => Status::Stable,
208                 _ => return None,
209             };
210             let name = parts.next().unwrap().trim();
211             let since = parts.next().unwrap().trim().trim_matches('"');
212             let issue_str = parts.next().unwrap().trim();
213             let tracking_issue = if issue_str.starts_with("None") {
214                 if level == Status::Unstable && !next_feature_is_rustc_internal {
215                     *bad = true;
216                     tidy_error!(
217                         bad,
218                         "libsyntax/feature_gate.rs:{}: no tracking issue for feature {}",
219                         line_number,
220                         name,
221                     );
222                 }
223                 None
224             } else {
225                 next_feature_is_rustc_internal = false;
226                 let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap();
227                 Some(s.parse().unwrap())
228             };
229             Some((name.to_owned(),
230                 Feature {
231                     level,
232                     since: since.to_owned(),
233                     has_gate_test: false,
234                     tracking_issue,
235                 }))
236         })
237         .collect()
238 }
239
240 pub fn collect_lib_features(base_src_path: &Path) -> Features {
241     let mut lib_features = Features::new();
242
243     // This library feature is defined in the `compiler_builtins` crate, which
244     // has been moved out-of-tree. Now it can no longer be auto-discovered by
245     // `tidy`, because we need to filter out its (submodule) directory. Manually
246     // add it to the set of known library features so we can still generate docs.
247     lib_features.insert("compiler_builtins_lib".to_owned(), Feature {
248         level: Status::Unstable,
249         since: String::new(),
250         has_gate_test: false,
251         tracking_issue: None,
252     });
253
254     map_lib_features(base_src_path,
255                      &mut |res, _, _| {
256         if let Ok((name, feature)) = res {
257             if lib_features.contains_key(name) {
258                 return;
259             }
260             lib_features.insert(name.to_owned(), feature);
261         }
262     });
263    lib_features
264 }
265
266 fn get_and_check_lib_features(base_src_path: &Path,
267                               bad: &mut bool,
268                               lang_features: &Features) -> Features {
269     let mut lib_features = Features::new();
270     map_lib_features(base_src_path,
271                      &mut |res, file, line| {
272             match res {
273                 Ok((name, f)) => {
274                     let mut check_features = |f: &Feature, list: &Features, display: &str| {
275                         if let Some(ref s) = list.get(name) {
276                             if f.tracking_issue != s.tracking_issue {
277                                 tidy_error!(bad,
278                                             "{}:{}: mismatches the `issue` in {}",
279                                             file.display(),
280                                             line,
281                                             display);
282                             }
283                         }
284                     };
285                     check_features(&f, &lang_features, "corresponding lang feature");
286                     check_features(&f, &lib_features, "previous");
287                     lib_features.insert(name.to_owned(), f);
288                 },
289                 Err(msg) => {
290                     tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
291                 },
292             }
293
294     });
295     lib_features
296 }
297
298 fn map_lib_features(base_src_path: &Path,
299                     mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) {
300     let mut contents = String::new();
301     super::walk(base_src_path,
302                 &mut |path| super::filter_dirs(path) || path.ends_with("src/test"),
303                 &mut |file| {
304         let filename = file.file_name().unwrap().to_string_lossy();
305         if !filename.ends_with(".rs") || filename == "features.rs" ||
306            filename == "diagnostic_list.rs" {
307             return;
308         }
309
310         contents.truncate(0);
311         t!(t!(File::open(&file), &file).read_to_string(&mut contents));
312
313         let mut becoming_feature: Option<(String, Feature)> = None;
314         for (i, line) in contents.lines().enumerate() {
315             macro_rules! err {
316                 ($msg:expr) => {{
317                     mf(Err($msg), file, i + 1);
318                     continue;
319                 }};
320             };
321             if let Some((ref name, ref mut f)) = becoming_feature {
322                 if f.tracking_issue.is_none() {
323                     f.tracking_issue = find_attr_val(line, "issue")
324                     .map(|s| s.parse().unwrap());
325                 }
326                 if line.ends_with(']') {
327                     mf(Ok((name, f.clone())), file, i + 1);
328                 } else if !line.ends_with(',') && !line.ends_with('\\') {
329                     // We need to bail here because we might have missed the
330                     // end of a stability attribute above because the ']'
331                     // might not have been at the end of the line.
332                     // We could then get into the very unfortunate situation that
333                     // we continue parsing the file assuming the current stability
334                     // attribute has not ended, and ignoring possible feature
335                     // attributes in the process.
336                     err!("malformed stability attribute");
337                 } else {
338                     continue;
339                 }
340             }
341             becoming_feature = None;
342             if line.contains("rustc_const_unstable(") {
343                 // const fn features are handled specially
344                 let feature_name = match find_attr_val(line, "feature") {
345                     Some(name) => name,
346                     None => err!("malformed stability attribute"),
347                 };
348                 let feature = Feature {
349                     level: Status::Unstable,
350                     since: "None".to_owned(),
351                     has_gate_test: false,
352                     // Whether there is a common tracking issue
353                     // for these feature gates remains an open question
354                     // https://github.com/rust-lang/rust/issues/24111#issuecomment-340283184
355                     // But we take 24111 otherwise they will be shown as
356                     // "internal to the compiler" which they are not.
357                     tracking_issue: Some(24111),
358                 };
359                 mf(Ok((feature_name, feature)), file, i + 1);
360                 continue;
361             }
362             let level = if line.contains("[unstable(") {
363                 Status::Unstable
364             } else if line.contains("[stable(") {
365                 Status::Stable
366             } else {
367                 continue;
368             };
369             let feature_name = match find_attr_val(line, "feature") {
370                 Some(name) => name,
371                 None => err!("malformed stability attribute"),
372             };
373             let since = match find_attr_val(line, "since") {
374                 Some(name) => name,
375                 None if level == Status::Stable => {
376                     err!("malformed stability attribute");
377                 }
378                 None => "None",
379             };
380             let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap());
381
382             let feature = Feature {
383                 level,
384                 since: since.to_owned(),
385                 has_gate_test: false,
386                 tracking_issue,
387             };
388             if line.contains(']') {
389                 mf(Ok((feature_name, feature)), file, i + 1);
390             } else {
391                 becoming_feature = Some((feature_name.to_owned(), feature));
392             }
393         }
394     });
395 }