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