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