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