]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/features.rs
Cache Regex's
[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;
19
20 mod version;
21 use 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, verbose: 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
136     if verbose {
137         let mut lines = Vec::new();
138         lines.extend(format_features(&features, "lang"));
139         lines.extend(format_features(&lib_features, "lib"));
140
141         lines.sort();
142         for line in lines {
143             println!("* {}", line);
144         }
145     } else {
146         println!("* {} features", features.len());
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.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     lazy_static::lazy_static! {
163         static ref ISSUE: Regex = Regex::new(r#"issue\s*=\s*"([^"]*)""#).unwrap();
164         static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap();
165         static ref SINCE: Regex = Regex::new(r#"since\s*=\s*"([^"]*)""#).unwrap();
166     }
167
168     let r = match attr {
169         "issue" => &*ISSUE,
170         "feature" => &*FEATURE,
171         "since" => &*SINCE,
172         _ => unimplemented!("{} not handled", attr),
173     };
174
175     r.captures(line)
176         .and_then(|c| c.get(1))
177         .map(|m| m.as_str())
178 }
179
180 #[test]
181 fn test_find_attr_val() {
182     let s = r#"#[unstable(feature = "checked_duration_since", issue = "58402")]"#;
183     assert_eq!(find_attr_val(s, "feature"), Some("checked_duration_since"));
184     assert_eq!(find_attr_val(s, "issue"), Some("58402"));
185     assert_eq!(find_attr_val(s, "since"), None);
186 }
187
188 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
189     if filen_underscore.starts_with("feature_gate") {
190         for (n, f) in features.iter_mut() {
191             if filen_underscore == format!("feature_gate_{}", n) {
192                 f.has_gate_test = true;
193                 return true;
194             }
195         }
196     }
197     return false;
198 }
199
200 pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
201     let contents = t!(fs::read_to_string(base_src_path.join("libsyntax/feature_gate.rs")));
202
203     // We allow rustc-internal features to omit a tracking issue.
204     // To make tidy accept omitting a tracking issue, group the list of features
205     // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
206     let mut next_feature_omits_tracking_issue = false;
207
208     let mut in_feature_group = false;
209     let mut prev_since = None;
210
211     contents.lines().zip(1..)
212         .filter_map(|(line, line_number)| {
213             let line = line.trim();
214
215             // Within -start and -end, the tracking issue can be omitted.
216             match line {
217                 "// no-tracking-issue-start" => {
218                     next_feature_omits_tracking_issue = true;
219                     return None;
220                 }
221                 "// no-tracking-issue-end" => {
222                     next_feature_omits_tracking_issue = false;
223                     return None;
224                 }
225                 _ => {}
226             }
227
228             if line.starts_with(FEATURE_GROUP_START_PREFIX) {
229                 if in_feature_group {
230                     tidy_error!(
231                         bad,
232                         // ignore-tidy-linelength
233                         "libsyntax/feature_gate.rs:{}: new feature group is started without ending the previous one",
234                         line_number,
235                     );
236                 }
237
238                 in_feature_group = true;
239                 prev_since = None;
240                 return None;
241             } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
242                 in_feature_group = false;
243                 prev_since = None;
244                 return None;
245             }
246
247             let mut parts = line.split(',');
248             let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
249                 Some("active") => Status::Unstable,
250                 Some("removed") => Status::Removed,
251                 Some("accepted") => Status::Stable,
252                 _ => return None,
253             };
254             let name = parts.next().unwrap().trim();
255
256             let since_str = parts.next().unwrap().trim().trim_matches('"');
257             let since = match since_str.parse() {
258                 Ok(since) => Some(since),
259                 Err(err) => {
260                     tidy_error!(
261                         bad,
262                         "libsyntax/feature_gate.rs:{}: failed to parse since: {} ({:?})",
263                         line_number,
264                         since_str,
265                         err,
266                     );
267                     None
268                 }
269             };
270             if in_feature_group {
271                 if prev_since > since {
272                     tidy_error!(
273                         bad,
274                         "libsyntax/feature_gate.rs:{}: feature {} is not sorted by since",
275                         line_number,
276                         name,
277                     );
278                 }
279                 prev_since = since;
280             }
281
282             let issue_str = parts.next().unwrap().trim();
283             let tracking_issue = if issue_str.starts_with("None") {
284                 if level == Status::Unstable && !next_feature_omits_tracking_issue {
285                     *bad = true;
286                     tidy_error!(
287                         bad,
288                         "libsyntax/feature_gate.rs:{}: no tracking issue for feature {}",
289                         line_number,
290                         name,
291                     );
292                 }
293                 None
294             } else {
295                 let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap();
296                 Some(s.parse().unwrap())
297             };
298             Some((name.to_owned(),
299                 Feature {
300                     level,
301                     since,
302                     has_gate_test: false,
303                     tracking_issue,
304                 }))
305         })
306         .collect()
307 }
308
309 pub fn collect_lib_features(base_src_path: &Path) -> Features {
310     let mut lib_features = Features::new();
311
312     // This library feature is defined in the `compiler_builtins` crate, which
313     // has been moved out-of-tree. Now it can no longer be auto-discovered by
314     // `tidy`, because we need to filter out its (submodule) directory. Manually
315     // add it to the set of known library features so we can still generate docs.
316     lib_features.insert("compiler_builtins_lib".to_owned(), Feature {
317         level: Status::Unstable,
318         since: None,
319         has_gate_test: false,
320         tracking_issue: None,
321     });
322
323     map_lib_features(base_src_path,
324                      &mut |res, _, _| {
325         if let Ok((name, feature)) = res {
326             if lib_features.contains_key(name) {
327                 return;
328             }
329             lib_features.insert(name.to_owned(), feature);
330         }
331     });
332    lib_features
333 }
334
335 fn get_and_check_lib_features(base_src_path: &Path,
336                               bad: &mut bool,
337                               lang_features: &Features) -> Features {
338     let mut lib_features = Features::new();
339     map_lib_features(base_src_path,
340                      &mut |res, file, line| {
341             match res {
342                 Ok((name, f)) => {
343                     let mut check_features = |f: &Feature, list: &Features, display: &str| {
344                         if let Some(ref s) = list.get(name) {
345                             if f.tracking_issue != s.tracking_issue {
346                                 tidy_error!(bad,
347                                             "{}:{}: mismatches the `issue` in {}",
348                                             file.display(),
349                                             line,
350                                             display);
351                             }
352                         }
353                     };
354                     check_features(&f, &lang_features, "corresponding lang feature");
355                     check_features(&f, &lib_features, "previous");
356                     lib_features.insert(name.to_owned(), f);
357                 },
358                 Err(msg) => {
359                     tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
360                 },
361             }
362
363     });
364     lib_features
365 }
366
367 fn map_lib_features(base_src_path: &Path,
368                     mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) {
369     let mut contents = String::new();
370     super::walk(base_src_path,
371                 &mut |path| super::filter_dirs(path) || path.ends_with("src/test"),
372                 &mut |file| {
373         let filename = file.file_name().unwrap().to_string_lossy();
374         if !filename.ends_with(".rs") || filename == "features.rs" ||
375            filename == "diagnostic_list.rs" {
376             return;
377         }
378
379         contents.truncate(0);
380         t!(t!(File::open(&file), &file).read_to_string(&mut contents));
381
382         let mut becoming_feature: Option<(String, Feature)> = None;
383         for (i, line) in contents.lines().enumerate() {
384             macro_rules! err {
385                 ($msg:expr) => {{
386                     mf(Err($msg), file, i + 1);
387                     continue;
388                 }};
389             };
390             if let Some((ref name, ref mut f)) = becoming_feature {
391                 if f.tracking_issue.is_none() {
392                     f.tracking_issue = find_attr_val(line, "issue")
393                     .map(|s| s.parse().unwrap());
394                 }
395                 if line.ends_with(']') {
396                     mf(Ok((name, f.clone())), file, i + 1);
397                 } else if !line.ends_with(',') && !line.ends_with('\\') {
398                     // We need to bail here because we might have missed the
399                     // end of a stability attribute above because the ']'
400                     // might not have been at the end of the line.
401                     // We could then get into the very unfortunate situation that
402                     // we continue parsing the file assuming the current stability
403                     // attribute has not ended, and ignoring possible feature
404                     // attributes in the process.
405                     err!("malformed stability attribute");
406                 } else {
407                     continue;
408                 }
409             }
410             becoming_feature = None;
411             if line.contains("rustc_const_unstable(") {
412                 // `const fn` features are handled specially.
413                 let feature_name = match find_attr_val(line, "feature") {
414                     Some(name) => name,
415                     None => err!("malformed stability attribute: missing `feature` key"),
416                 };
417                 let feature = Feature {
418                     level: Status::Unstable,
419                     since: None,
420                     has_gate_test: false,
421                     // FIXME(#57563): #57563 is now used as a common tracking issue,
422                     // although we would like to have specific tracking issues for each
423                     // `rustc_const_unstable` in the future.
424                     tracking_issue: Some(57563),
425                 };
426                 mf(Ok((feature_name, feature)), file, i + 1);
427                 continue;
428             }
429             let level = if line.contains("[unstable(") {
430                 Status::Unstable
431             } else if line.contains("[stable(") {
432                 Status::Stable
433             } else {
434                 continue;
435             };
436             let feature_name = match find_attr_val(line, "feature") {
437                 Some(name) => name,
438                 None => err!("malformed stability attribute: missing `feature` key"),
439             };
440             let since = match find_attr_val(line, "since").map(|x| x.parse()) {
441                 Some(Ok(since)) => Some(since),
442                 Some(Err(_err)) => {
443                     err!("malformed stability attribute: can't parse `since` key");
444                 },
445                 None if level == Status::Stable => {
446                     err!("malformed stability attribute: missing the `since` key");
447                 }
448                 None => None,
449             };
450             let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap());
451
452             let feature = Feature {
453                 level,
454                 since,
455                 has_gate_test: false,
456                 tracking_issue,
457             };
458             if line.contains(']') {
459                 mf(Ok((feature_name, feature)), file, i + 1);
460             } else {
461                 becoming_feature = Some((feature_name.to_owned(), feature));
462             }
463         }
464     });
465 }