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