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