1 //! Tidy check to ensure that unstable features are all in order.
3 //! This check will ensure properties like:
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.
12 use std::collections::HashMap;
14 use std::fs::{self, File};
15 use std::io::prelude::*;
18 use regex::{Regex, escape};
21 use self::version::Version;
23 const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start:";
24 const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
26 #[derive(Debug, PartialEq, Clone)]
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",
40 fmt::Display::fmt(as_str, f)
44 #[derive(Debug, Clone)]
47 pub since: Option<Version>,
48 pub has_gate_test: bool,
49 pub tracking_issue: Option<u32>,
52 pub type Features = HashMap<String, Feature>;
54 pub fn check(path: &Path, bad: &mut bool, quiet: bool) {
55 let mut features = collect_lang_features(path, bad);
56 assert!(!features.is_empty());
58 let lib_features = get_and_check_lib_features(path, bad, &features);
59 assert!(!lib_features.is_empty());
61 let mut contents = String::new();
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),
68 let filename = file.file_name().unwrap().to_string_lossy();
69 if !filename.ends_with(".rs") || filename == "features.rs" ||
70 filename == "diagnostic_list.rs" {
74 let filen_underscore = filename.replace('-',"_").replace(".rs","");
75 let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
78 t!(t!(File::open(&file), &file).read_to_string(&mut contents));
80 for (i, line) in contents.lines().enumerate() {
81 let mut err = |msg: &str| {
82 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
85 let gate_test_str = "gate-test-";
87 let feature_name = match line.find(gate_test_str) {
89 line[i+gate_test_str.len()..].splitn(2, ' ').next().unwrap()
93 match features.get_mut(feature_name) {
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",
101 f.has_gate_test = true;
104 err(&format!("gate-test test found referencing a nonexistent feature '{}'",
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<_>>();
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.",
128 if !gate_untested.is_empty() {
129 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
136 println!("* {} features", features.len());
140 let mut lines = Vec::new();
141 lines.extend(format_features(&features, "lang"));
142 lines.extend(format_features(&lib_features, "lib"));
146 println!("* {}", line);
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}",
156 feature.since.as_ref().map_or("None".to_owned(),
157 |since| since.to_string()))
161 fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
162 let r = Regex::new(&format!(r#"{}\s*=\s*"([^"]*)""#, escape(attr)))
163 .expect("malformed regex for find_attr_val");
165 .and_then(|c| c.get(1))
170 fn test_find_attr_val() {
171 let s = r#"#[unstable(feature = "checked_duration_since", issue = "58402")]"#;
172 assert_eq!(find_attr_val(s, "feature"), Some("checked_duration_since"));
173 assert_eq!(find_attr_val(s, "issue"), Some("58402"));
174 assert_eq!(find_attr_val(s, "since"), None);
177 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
178 if filen_underscore.starts_with("feature_gate") {
179 for (n, f) in features.iter_mut() {
180 if filen_underscore == format!("feature_gate_{}", n) {
181 f.has_gate_test = true;
189 pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
190 let contents = t!(fs::read_to_string(base_src_path.join("libsyntax/feature_gate.rs")));
192 // We allow rustc-internal features to omit a tracking issue.
193 // To make tidy accept omitting a tracking issue, group the list of features
194 // without one inside `// no tracking issue START` and `// no tracking issue END`.
195 let mut next_feature_omits_tracking_issue = false;
197 let mut next_feature_group = None;
198 let mut prev_since = None;
200 contents.lines().zip(1..)
201 .filter_map(|(line, line_number)| {
202 let line = line.trim();
204 // Within START and END, the tracking issue can be omitted.
206 "// no tracking issue START" => {
207 next_feature_omits_tracking_issue = true;
210 "// no tracking issue END" => {
211 next_feature_omits_tracking_issue = false;
217 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
218 if next_feature_group.is_some() {
221 // ignore-tidy-linelength
222 "libsyntax/feature_gate.rs:{}: new feature group is started without ending the previous one",
227 let group = line.trim_start_matches(FEATURE_GROUP_START_PREFIX).trim();
228 next_feature_group = Some(group.to_owned());
231 } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
232 next_feature_group = None;
237 let mut parts = line.split(',');
238 let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
239 Some("active") => Status::Unstable,
240 Some("removed") => Status::Removed,
241 Some("accepted") => Status::Stable,
244 let name = parts.next().unwrap().trim();
246 let since_str = parts.next().unwrap().trim().trim_matches('"');
247 let since = match since_str.parse() {
248 Ok(since) => Some(since),
252 "libsyntax/feature_gate.rs:{}: failed to parse since: {} ({})",
260 if next_feature_group.is_some() {
261 if prev_since > since {
264 "libsyntax/feature_gate.rs:{}: feature {} is not sorted by since",
269 prev_since = since.clone();
272 let issue_str = parts.next().unwrap().trim();
273 let tracking_issue = if issue_str.starts_with("None") {
274 if level == Status::Unstable && !next_feature_omits_tracking_issue {
278 "libsyntax/feature_gate.rs:{}: no tracking issue for feature {}",
285 let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap();
286 Some(s.parse().unwrap())
288 Some((name.to_owned(),
292 has_gate_test: false,
299 pub fn collect_lib_features(base_src_path: &Path) -> Features {
300 let mut lib_features = Features::new();
302 // This library feature is defined in the `compiler_builtins` crate, which
303 // has been moved out-of-tree. Now it can no longer be auto-discovered by
304 // `tidy`, because we need to filter out its (submodule) directory. Manually
305 // add it to the set of known library features so we can still generate docs.
306 lib_features.insert("compiler_builtins_lib".to_owned(), Feature {
307 level: Status::Unstable,
309 has_gate_test: false,
310 tracking_issue: None,
313 map_lib_features(base_src_path,
315 if let Ok((name, feature)) = res {
316 if lib_features.contains_key(name) {
319 lib_features.insert(name.to_owned(), feature);
325 fn get_and_check_lib_features(base_src_path: &Path,
327 lang_features: &Features) -> Features {
328 let mut lib_features = Features::new();
329 map_lib_features(base_src_path,
330 &mut |res, file, line| {
333 let mut check_features = |f: &Feature, list: &Features, display: &str| {
334 if let Some(ref s) = list.get(name) {
335 if f.tracking_issue != s.tracking_issue {
337 "{}:{}: mismatches the `issue` in {}",
344 check_features(&f, &lang_features, "corresponding lang feature");
345 check_features(&f, &lib_features, "previous");
346 lib_features.insert(name.to_owned(), f);
349 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
357 fn map_lib_features(base_src_path: &Path,
358 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) {
359 let mut contents = String::new();
360 super::walk(base_src_path,
361 &mut |path| super::filter_dirs(path) || path.ends_with("src/test"),
363 let filename = file.file_name().unwrap().to_string_lossy();
364 if !filename.ends_with(".rs") || filename == "features.rs" ||
365 filename == "diagnostic_list.rs" {
369 contents.truncate(0);
370 t!(t!(File::open(&file), &file).read_to_string(&mut contents));
372 let mut becoming_feature: Option<(String, Feature)> = None;
373 for (i, line) in contents.lines().enumerate() {
376 mf(Err($msg), file, i + 1);
380 if let Some((ref name, ref mut f)) = becoming_feature {
381 if f.tracking_issue.is_none() {
382 f.tracking_issue = find_attr_val(line, "issue")
383 .map(|s| s.parse().unwrap());
385 if line.ends_with(']') {
386 mf(Ok((name, f.clone())), file, i + 1);
387 } else if !line.ends_with(',') && !line.ends_with('\\') {
388 // We need to bail here because we might have missed the
389 // end of a stability attribute above because the ']'
390 // might not have been at the end of the line.
391 // We could then get into the very unfortunate situation that
392 // we continue parsing the file assuming the current stability
393 // attribute has not ended, and ignoring possible feature
394 // attributes in the process.
395 err!("malformed stability attribute");
400 becoming_feature = None;
401 if line.contains("rustc_const_unstable(") {
402 // `const fn` features are handled specially.
403 let feature_name = match find_attr_val(line, "feature") {
405 None => err!("malformed stability attribute: missing `feature` key"),
407 let feature = Feature {
408 level: Status::Unstable,
410 has_gate_test: false,
411 // FIXME(#57563): #57563 is now used as a common tracking issue,
412 // although we would like to have specific tracking issues for each
413 // `rustc_const_unstable` in the future.
414 tracking_issue: Some(57563),
416 mf(Ok((feature_name, feature)), file, i + 1);
419 let level = if line.contains("[unstable(") {
421 } else if line.contains("[stable(") {
426 let feature_name = match find_attr_val(line, "feature") {
428 None => err!("malformed stability attribute: missing `feature` key"),
430 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
431 Some(Ok(since)) => Some(since),
433 err!("malformed stability attribute: can't parse `since` key");
435 None if level == Status::Stable => {
436 err!("malformed stability attribute: missing the `since` key");
440 let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap());
442 let feature = Feature {
445 has_gate_test: false,
448 if line.contains(']') {
449 mf(Ok((feature_name, feature)), file, i + 1);
451 becoming_feature = Some((feature_name.to_owned(), feature));