]> git.lizzy.rs Git - rust.git/blob - src/tools/tidy/src/features.rs
Bless NLL test
[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;
15 use std::num::NonZeroU32;
16 use std::path::Path;
17
18 use regex::Regex;
19
20 #[cfg(test)]
21 mod tests;
22
23 mod version;
24 use version::Version;
25
26 const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
27 const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
28
29 #[derive(Debug, PartialEq, Clone)]
30 pub enum Status {
31     Stable,
32     Removed,
33     Unstable,
34 }
35
36 impl fmt::Display for Status {
37     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38         let as_str = match *self {
39             Status::Stable => "stable",
40             Status::Unstable => "unstable",
41             Status::Removed => "removed",
42         };
43         fmt::Display::fmt(as_str, f)
44     }
45 }
46
47 #[derive(Debug, Clone)]
48 pub struct Feature {
49     pub level: Status,
50     pub since: Option<Version>,
51     pub has_gate_test: bool,
52     pub tracking_issue: Option<NonZeroU32>,
53 }
54
55 pub type Features = HashMap<String, Feature>;
56
57 pub struct CollectedFeatures {
58     pub lib: Features,
59     pub lang: Features,
60 }
61
62 // Currently only used for unstable book generation
63 pub fn collect_lib_features(base_src_path: &Path) -> Features {
64     let mut lib_features = Features::new();
65
66     map_lib_features(base_src_path, &mut |res, _, _| {
67         if let Ok((name, feature)) = res {
68             lib_features.insert(name.to_owned(), feature);
69         }
70     });
71     lib_features
72 }
73
74 pub fn check(
75     src_path: &Path,
76     compiler_path: &Path,
77     lib_path: &Path,
78     bad: &mut bool,
79     verbose: bool,
80 ) -> CollectedFeatures {
81     let mut features = collect_lang_features(compiler_path, bad);
82     assert!(!features.is_empty());
83
84     let lib_features = get_and_check_lib_features(lib_path, bad, &features);
85     assert!(!lib_features.is_empty());
86
87     super::walk_many(
88         &[
89             &src_path.join("test/ui"),
90             &src_path.join("test/ui-fulldeps"),
91             &src_path.join("test/compile-fail"),
92         ],
93         &mut |path| super::filter_dirs(path),
94         &mut |entry, contents| {
95             let file = entry.path();
96             let filename = file.file_name().unwrap().to_string_lossy();
97             if !filename.ends_with(".rs")
98                 || filename == "features.rs"
99                 || filename == "diagnostic_list.rs"
100             {
101                 return;
102             }
103
104             let filen_underscore = filename.replace('-', "_").replace(".rs", "");
105             let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
106
107             for (i, line) in contents.lines().enumerate() {
108                 let mut err = |msg: &str| {
109                     tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
110                 };
111
112                 let gate_test_str = "gate-test-";
113
114                 let feature_name = match line.find(gate_test_str) {
115                     Some(i) => line[i + gate_test_str.len()..].splitn(2, ' ').next().unwrap(),
116                     None => continue,
117                 };
118                 match features.get_mut(feature_name) {
119                     Some(f) => {
120                         if filename_is_gate_test {
121                             err(&format!(
122                                 "The file is already marked as gate test \
123                                       through its name, no need for a \
124                                       'gate-test-{}' comment",
125                                 feature_name
126                             ));
127                         }
128                         f.has_gate_test = true;
129                     }
130                     None => {
131                         err(&format!(
132                             "gate-test test found referencing a nonexistent feature '{}'",
133                             feature_name
134                         ));
135                     }
136                 }
137             }
138         },
139     );
140
141     // Only check the number of lang features.
142     // Obligatory testing for library features is dumb.
143     let gate_untested = features
144         .iter()
145         .filter(|&(_, f)| f.level == Status::Unstable)
146         .filter(|&(_, f)| !f.has_gate_test)
147         .collect::<Vec<_>>();
148
149     for &(name, _) in gate_untested.iter() {
150         println!("Expected a gate test for the feature '{}'.", name);
151         println!(
152             "Hint: create a failing test file named 'feature-gate-{}.rs'\
153                 \n      in the 'ui' test suite, with its failures due to\
154                 \n      missing usage of `#![feature({})]`.",
155             name, name
156         );
157         println!(
158             "Hint: If you already have such a test and don't want to rename it,\
159                 \n      you can also add a // gate-test-{} line to the test file.",
160             name
161         );
162     }
163
164     if !gate_untested.is_empty() {
165         tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
166     }
167
168     if *bad {
169         return CollectedFeatures { lib: lib_features, lang: features };
170     }
171
172     if verbose {
173         let mut lines = Vec::new();
174         lines.extend(format_features(&features, "lang"));
175         lines.extend(format_features(&lib_features, "lib"));
176
177         lines.sort();
178         for line in lines {
179             println!("* {}", line);
180         }
181     } else {
182         println!("* {} features", features.len());
183     }
184
185     CollectedFeatures { lib: lib_features, lang: features }
186 }
187
188 fn format_features<'a>(
189     features: &'a Features,
190     family: &'a str,
191 ) -> impl Iterator<Item = String> + 'a {
192     features.iter().map(move |(name, feature)| {
193         format!(
194             "{:<32} {:<8} {:<12} {:<8}",
195             name,
196             family,
197             feature.level,
198             feature.since.map_or("None".to_owned(), |since| since.to_string())
199         )
200     })
201 }
202
203 fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
204     lazy_static::lazy_static! {
205         static ref ISSUE: Regex = Regex::new(r#"issue\s*=\s*"([^"]*)""#).unwrap();
206         static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap();
207         static ref SINCE: Regex = Regex::new(r#"since\s*=\s*"([^"]*)""#).unwrap();
208     }
209
210     let r = match attr {
211         "issue" => &*ISSUE,
212         "feature" => &*FEATURE,
213         "since" => &*SINCE,
214         _ => unimplemented!("{} not handled", attr),
215     };
216
217     r.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
218 }
219
220 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
221     let prefix = "feature_gate_";
222     if filen_underscore.starts_with(prefix) {
223         for (n, f) in features.iter_mut() {
224             // Equivalent to filen_underscore == format!("feature_gate_{}", n)
225             if &filen_underscore[prefix.len()..] == n {
226                 f.has_gate_test = true;
227                 return true;
228             }
229         }
230     }
231     false
232 }
233
234 pub fn collect_lang_features(base_compiler_path: &Path, bad: &mut bool) -> Features {
235     let mut all = collect_lang_features_in(base_compiler_path, "active.rs", bad);
236     all.extend(collect_lang_features_in(base_compiler_path, "accepted.rs", bad));
237     all.extend(collect_lang_features_in(base_compiler_path, "removed.rs", bad));
238     all
239 }
240
241 fn collect_lang_features_in(base: &Path, file: &str, bad: &mut bool) -> Features {
242     let path = base.join("rustc_feature").join("src").join(file);
243     let contents = t!(fs::read_to_string(&path));
244
245     // We allow rustc-internal features to omit a tracking issue.
246     // To make tidy accept omitting a tracking issue, group the list of features
247     // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
248     let mut next_feature_omits_tracking_issue = false;
249
250     let mut in_feature_group = false;
251     let mut prev_since = None;
252
253     contents
254         .lines()
255         .zip(1..)
256         .filter_map(|(line, line_number)| {
257             let line = line.trim();
258
259             // Within -start and -end, the tracking issue can be omitted.
260             match line {
261                 "// no-tracking-issue-start" => {
262                     next_feature_omits_tracking_issue = true;
263                     return None;
264                 }
265                 "// no-tracking-issue-end" => {
266                     next_feature_omits_tracking_issue = false;
267                     return None;
268                 }
269                 _ => {}
270             }
271
272             if line.starts_with(FEATURE_GROUP_START_PREFIX) {
273                 if in_feature_group {
274                     tidy_error!(
275                         bad,
276                         "{}:{}: \
277                         new feature group is started without ending the previous one",
278                         path.display(),
279                         line_number,
280                     );
281                 }
282
283                 in_feature_group = true;
284                 prev_since = None;
285                 return None;
286             } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
287                 in_feature_group = false;
288                 prev_since = None;
289                 return None;
290             }
291
292             let mut parts = line.split(',');
293             let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
294                 Some("active") => Status::Unstable,
295                 Some("removed") => Status::Removed,
296                 Some("accepted") => Status::Stable,
297                 _ => return None,
298             };
299             let name = parts.next().unwrap().trim();
300
301             let since_str = parts.next().unwrap().trim().trim_matches('"');
302             let since = match since_str.parse() {
303                 Ok(since) => Some(since),
304                 Err(err) => {
305                     tidy_error!(
306                         bad,
307                         "{}:{}: failed to parse since: {} ({:?})",
308                         path.display(),
309                         line_number,
310                         since_str,
311                         err,
312                     );
313                     None
314                 }
315             };
316             if in_feature_group {
317                 if prev_since > since {
318                     tidy_error!(
319                         bad,
320                         "{}:{}: feature {} is not sorted by \"since\" (version number)",
321                         path.display(),
322                         line_number,
323                         name,
324                     );
325                 }
326                 prev_since = since;
327             }
328
329             let issue_str = parts.next().unwrap().trim();
330             let tracking_issue = if issue_str.starts_with("None") {
331                 if level == Status::Unstable && !next_feature_omits_tracking_issue {
332                     *bad = true;
333                     tidy_error!(
334                         bad,
335                         "{}:{}: no tracking issue for feature {}",
336                         path.display(),
337                         line_number,
338                         name,
339                     );
340                 }
341                 None
342             } else {
343                 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
344                 Some(s.parse().unwrap())
345             };
346             Some((name.to_owned(), Feature { level, since, has_gate_test: false, tracking_issue }))
347         })
348         .collect()
349 }
350
351 fn get_and_check_lib_features(
352     base_src_path: &Path,
353     bad: &mut bool,
354     lang_features: &Features,
355 ) -> Features {
356     let mut lib_features = Features::new();
357     map_lib_features(base_src_path, &mut |res, file, line| match res {
358         Ok((name, f)) => {
359             let mut check_features = |f: &Feature, list: &Features, display: &str| {
360                 if let Some(ref s) = list.get(name) {
361                     if f.tracking_issue != s.tracking_issue && f.level != Status::Stable {
362                         tidy_error!(
363                             bad,
364                             "{}:{}: mismatches the `issue` in {}",
365                             file.display(),
366                             line,
367                             display
368                         );
369                     }
370                 }
371             };
372             check_features(&f, &lang_features, "corresponding lang feature");
373             check_features(&f, &lib_features, "previous");
374             lib_features.insert(name.to_owned(), f);
375         }
376         Err(msg) => {
377             tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
378         }
379     });
380     lib_features
381 }
382
383 fn map_lib_features(
384     base_src_path: &Path,
385     mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize),
386 ) {
387     super::walk(
388         base_src_path,
389         &mut |path| super::filter_dirs(path) || path.ends_with("src/test"),
390         &mut |entry, contents| {
391             let file = entry.path();
392             let filename = file.file_name().unwrap().to_string_lossy();
393             if !filename.ends_with(".rs")
394                 || filename == "features.rs"
395                 || filename == "diagnostic_list.rs"
396                 || filename == "error_codes.rs"
397             {
398                 return;
399             }
400
401             // This is an early exit -- all the attributes we're concerned with must contain this:
402             // * rustc_const_unstable(
403             // * unstable(
404             // * stable(
405             if !contents.contains("stable(") {
406                 return;
407             }
408
409             let handle_issue_none = |s| match s {
410                 "none" => None,
411                 issue => {
412                     let n = issue.parse().expect("issue number is not a valid integer");
413                     assert_ne!(n, 0, "\"none\" should be used when there is no issue, not \"0\"");
414                     NonZeroU32::new(n)
415                 }
416             };
417             let mut becoming_feature: Option<(&str, Feature)> = None;
418             let mut iter_lines = contents.lines().enumerate().peekable();
419             while let Some((i, line)) = iter_lines.next() {
420                 macro_rules! err {
421                     ($msg:expr) => {{
422                         mf(Err($msg), file, i + 1);
423                         continue;
424                     }};
425                 };
426                 if let Some((ref name, ref mut f)) = becoming_feature {
427                     if f.tracking_issue.is_none() {
428                         f.tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
429                     }
430                     if line.ends_with(']') {
431                         mf(Ok((name, f.clone())), file, i + 1);
432                     } else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"')
433                     {
434                         // We need to bail here because we might have missed the
435                         // end of a stability attribute above because the ']'
436                         // might not have been at the end of the line.
437                         // We could then get into the very unfortunate situation that
438                         // we continue parsing the file assuming the current stability
439                         // attribute has not ended, and ignoring possible feature
440                         // attributes in the process.
441                         err!("malformed stability attribute");
442                     } else {
443                         continue;
444                     }
445                 }
446                 becoming_feature = None;
447                 if line.contains("rustc_const_unstable(") {
448                     // `const fn` features are handled specially.
449                     let feature_name = match find_attr_val(line, "feature") {
450                         Some(name) => name,
451                         None => err!("malformed stability attribute: missing `feature` key"),
452                     };
453                     let feature = Feature {
454                         level: Status::Unstable,
455                         since: None,
456                         has_gate_test: false,
457                         tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
458                     };
459                     mf(Ok((feature_name, feature)), file, i + 1);
460                     continue;
461                 }
462                 let level = if line.contains("[unstable(") {
463                     Status::Unstable
464                 } else if line.contains("[stable(") {
465                     Status::Stable
466                 } else {
467                     continue;
468                 };
469                 let feature_name = match find_attr_val(line, "feature")
470                     .or_else(|| iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature")))
471                 {
472                     Some(name) => name,
473                     None => err!("malformed stability attribute: missing `feature` key"),
474                 };
475                 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
476                     Some(Ok(since)) => Some(since),
477                     Some(Err(_err)) => {
478                         err!("malformed stability attribute: can't parse `since` key");
479                     }
480                     None if level == Status::Stable => {
481                         err!("malformed stability attribute: missing the `since` key");
482                     }
483                     None => None,
484                 };
485                 let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
486
487                 let feature = Feature { level, since, has_gate_test: false, tracking_issue };
488                 if line.contains(']') {
489                     mf(Ok((feature_name, feature)), file, i + 1);
490                 } else {
491                     becoming_feature = Some((feature_name, feature));
492                 }
493             }
494         },
495     );
496 }