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