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 feature name.
12 use crate::walk::{filter_dirs, walk, walk_many};
13 use std::collections::HashMap;
16 use std::num::NonZeroU32;
27 const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
28 const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
30 #[derive(Debug, PartialEq, Clone)]
37 impl fmt::Display for Status {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 let as_str = match *self {
40 Status::Stable => "stable",
41 Status::Unstable => "unstable",
42 Status::Removed => "removed",
44 fmt::Display::fmt(as_str, f)
48 #[derive(Debug, Clone)]
51 pub since: Option<Version>,
52 pub has_gate_test: bool,
53 pub tracking_issue: Option<NonZeroU32>,
56 fn tracking_issue_display(&self) -> impl fmt::Display {
57 match self.tracking_issue {
58 None => "none".to_string(),
59 Some(x) => x.to_string(),
64 pub type Features = HashMap<String, Feature>;
66 pub struct CollectedFeatures {
71 // Currently only used for unstable book generation
72 pub fn collect_lib_features(base_src_path: &Path) -> Features {
73 let mut lib_features = Features::new();
75 map_lib_features(base_src_path, &mut |res, _, _| {
76 if let Ok((name, feature)) = res {
77 lib_features.insert(name.to_owned(), feature);
89 ) -> CollectedFeatures {
90 let mut features = collect_lang_features(compiler_path, bad);
91 assert!(!features.is_empty());
93 let lib_features = get_and_check_lib_features(lib_path, bad, &features);
94 assert!(!lib_features.is_empty());
98 &src_path.join("test/ui"),
99 &src_path.join("test/ui-fulldeps"),
100 &src_path.join("test/rustdoc-ui"),
101 &src_path.join("test/rustdoc"),
104 &mut |entry, contents| {
105 let file = entry.path();
106 let filename = file.file_name().unwrap().to_string_lossy();
107 if !filename.ends_with(".rs")
108 || filename == "features.rs"
109 || filename == "diagnostic_list.rs"
114 let filen_underscore = filename.replace('-', "_").replace(".rs", "");
115 let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
117 for (i, line) in contents.lines().enumerate() {
118 let mut err = |msg: &str| {
119 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
122 let gate_test_str = "gate-test-";
124 let feature_name = match line.find(gate_test_str) {
125 // NB: the `splitn` always succeeds, even if the delimiter is not present.
126 Some(i) => line[i + gate_test_str.len()..].splitn(2, ' ').next().unwrap(),
129 match features.get_mut(feature_name) {
131 if filename_is_gate_test {
133 "The file is already marked as gate test \
134 through its name, no need for a \
135 'gate-test-{}' comment",
139 f.has_gate_test = true;
143 "gate-test test found referencing a nonexistent feature '{}'",
152 // Only check the number of lang features.
153 // Obligatory testing for library features is dumb.
154 let gate_untested = features
156 .filter(|&(_, f)| f.level == Status::Unstable)
157 .filter(|&(_, f)| !f.has_gate_test)
158 .collect::<Vec<_>>();
160 for &(name, _) in gate_untested.iter() {
161 println!("Expected a gate test for the feature '{name}'.");
163 "Hint: create a failing test file named 'feature-gate-{}.rs'\
164 \n in the 'ui' test suite, with its failures due to\
165 \n missing usage of `#![feature({})]`.",
169 "Hint: If you already have such a test and don't want to rename it,\
170 \n you can also add a // gate-test-{} line to the test file.",
175 if !gate_untested.is_empty() {
176 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
179 let (version, channel) = get_version_and_channel(src_path);
181 let all_features_iter = features
183 .map(|feat| (feat, "lang"))
184 .chain(lib_features.iter().map(|feat| (feat, "lib")));
185 for ((feature_name, feature), kind) in all_features_iter {
186 let since = if let Some(since) = feature.since { since } else { continue };
187 if since > version && since != Version::CurrentPlaceholder {
190 "The stabilization version {since} of {kind} feature `{feature_name}` is newer than the current {version}"
193 if channel == "nightly" && since == version {
196 "The stabilization version {since} of {kind} feature `{feature_name}` is written out but should be {}",
197 version::VERSION_PLACEHOLDER
200 if channel != "nightly" && since == Version::CurrentPlaceholder {
203 "The placeholder use of {kind} feature `{feature_name}` is not allowed on the {channel} channel",
209 return CollectedFeatures { lib: lib_features, lang: features };
213 let mut lines = Vec::new();
214 lines.extend(format_features(&features, "lang"));
215 lines.extend(format_features(&lib_features, "lib"));
219 println!("* {line}");
222 println!("* {} features", features.len());
225 CollectedFeatures { lib: lib_features, lang: features }
228 fn get_version_and_channel(src_path: &Path) -> (Version, String) {
229 let version_str = t!(std::fs::read_to_string(src_path.join("version")));
230 let version_str = version_str.trim();
231 let version = t!(std::str::FromStr::from_str(&version_str).map_err(|e| format!("{e:?}")));
232 let channel_str = t!(std::fs::read_to_string(src_path.join("ci").join("channel")));
233 (version, channel_str.trim().to_owned())
236 fn format_features<'a>(
237 features: &'a Features,
239 ) -> impl Iterator<Item = String> + 'a {
240 features.iter().map(move |(name, feature)| {
242 "{:<32} {:<8} {:<12} {:<8}",
246 feature.since.map_or("None".to_owned(), |since| since.to_string())
251 fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
252 lazy_static::lazy_static! {
253 static ref ISSUE: Regex = Regex::new(r#"issue\s*=\s*"([^"]*)""#).unwrap();
254 static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap();
255 static ref SINCE: Regex = Regex::new(r#"since\s*=\s*"([^"]*)""#).unwrap();
260 "feature" => &*FEATURE,
262 _ => unimplemented!("{attr} not handled"),
265 r.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
268 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
269 let prefix = "feature_gate_";
270 if filen_underscore.starts_with(prefix) {
271 for (n, f) in features.iter_mut() {
272 // Equivalent to filen_underscore == format!("feature_gate_{n}")
273 if &filen_underscore[prefix.len()..] == n {
274 f.has_gate_test = true;
282 pub fn collect_lang_features(base_compiler_path: &Path, bad: &mut bool) -> Features {
283 let mut all = collect_lang_features_in(base_compiler_path, "active.rs", bad);
284 all.extend(collect_lang_features_in(base_compiler_path, "accepted.rs", bad));
285 all.extend(collect_lang_features_in(base_compiler_path, "removed.rs", bad));
289 fn collect_lang_features_in(base: &Path, file: &str, bad: &mut bool) -> Features {
290 let path = base.join("rustc_feature").join("src").join(file);
291 let contents = t!(fs::read_to_string(&path));
293 // We allow rustc-internal features to omit a tracking issue.
294 // To make tidy accept omitting a tracking issue, group the list of features
295 // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
296 let mut next_feature_omits_tracking_issue = false;
298 let mut in_feature_group = false;
299 let mut prev_names = vec![];
304 .filter_map(|(line, line_number)| {
305 let line = line.trim();
307 // Within -start and -end, the tracking issue can be omitted.
309 "// no-tracking-issue-start" => {
310 next_feature_omits_tracking_issue = true;
313 "// no-tracking-issue-end" => {
314 next_feature_omits_tracking_issue = false;
320 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
321 if in_feature_group {
325 new feature group is started without ending the previous one",
331 in_feature_group = true;
334 } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
335 in_feature_group = false;
340 let mut parts = line.split(',');
341 let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
342 Some("active") => Status::Unstable,
343 Some("incomplete") => Status::Unstable,
344 Some("removed") => Status::Removed,
345 Some("accepted") => Status::Stable,
348 let name = parts.next().unwrap().trim();
350 let since_str = parts.next().unwrap().trim().trim_matches('"');
351 let since = match since_str.parse() {
352 Ok(since) => Some(since),
356 "{}:{}: failed to parse since: {} ({:?})",
365 if in_feature_group {
366 if prev_names.last() > Some(&name) {
367 // This assumes the user adds the feature name at the end of the list, as we're
368 // not looking ahead.
369 let correct_index = match prev_names.binary_search(&name) {
371 // This only occurs when the feature name has already been declared.
374 "{}:{}: duplicate feature {}",
379 // skip any additional checks for this line
385 let correct_placement = if correct_index == 0 {
386 "at the beginning of the feature group".to_owned()
387 } else if correct_index == prev_names.len() {
388 // I don't believe this is reachable given the above assumption, but it
389 // doesn't hurt to be safe.
390 "at the end of the feature group".to_owned()
394 prev_names[correct_index - 1],
395 prev_names[correct_index],
401 "{}:{}: feature {} is not sorted by feature name (should be {})",
408 prev_names.push(name);
411 let issue_str = parts.next().unwrap().trim();
412 let tracking_issue = if issue_str.starts_with("None") {
413 if level == Status::Unstable && !next_feature_omits_tracking_issue {
416 "{}:{}: no tracking issue for feature {}",
424 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
425 Some(s.parse().unwrap())
427 Some((name.to_owned(), Feature { level, since, has_gate_test: false, tracking_issue }))
432 fn get_and_check_lib_features(
433 base_src_path: &Path,
435 lang_features: &Features,
437 let mut lib_features = Features::new();
438 map_lib_features(base_src_path, &mut |res, file, line| match res {
440 let mut check_features = |f: &Feature, list: &Features, display: &str| {
441 if let Some(ref s) = list.get(name) {
442 if f.tracking_issue != s.tracking_issue && f.level != Status::Stable {
445 "{}:{}: `issue` \"{}\" mismatches the {} `issue` of \"{}\"",
448 f.tracking_issue_display(),
450 s.tracking_issue_display(),
455 check_features(&f, &lang_features, "corresponding lang feature");
456 check_features(&f, &lib_features, "previous");
457 lib_features.insert(name.to_owned(), f);
460 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
467 base_src_path: &Path,
468 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize),
472 &mut |path| filter_dirs(path) || path.ends_with("src/test"),
473 &mut |entry, contents| {
474 let file = entry.path();
475 let filename = file.file_name().unwrap().to_string_lossy();
476 if !filename.ends_with(".rs")
477 || filename == "features.rs"
478 || filename == "diagnostic_list.rs"
479 || filename == "error_codes.rs"
484 // This is an early exit -- all the attributes we're concerned with must contain this:
485 // * rustc_const_unstable(
488 if !contents.contains("stable(") {
492 let handle_issue_none = |s| match s {
495 let n = issue.parse().expect("issue number is not a valid integer");
496 assert_ne!(n, 0, "\"none\" should be used when there is no issue, not \"0\"");
500 let mut becoming_feature: Option<(&str, Feature)> = None;
501 let mut iter_lines = contents.lines().enumerate().peekable();
502 while let Some((i, line)) = iter_lines.next() {
505 mf(Err($msg), file, i + 1);
510 lazy_static::lazy_static! {
511 static ref COMMENT_LINE: Regex = Regex::new(r"^\s*//").unwrap();
513 // exclude commented out lines
514 if COMMENT_LINE.is_match(line) {
518 if let Some((ref name, ref mut f)) = becoming_feature {
519 if f.tracking_issue.is_none() {
520 f.tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
522 if line.ends_with(']') {
523 mf(Ok((name, f.clone())), file, i + 1);
524 } else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"')
526 // We need to bail here because we might have missed the
527 // end of a stability attribute above because the ']'
528 // might not have been at the end of the line.
529 // We could then get into the very unfortunate situation that
530 // we continue parsing the file assuming the current stability
531 // attribute has not ended, and ignoring possible feature
532 // attributes in the process.
533 err!("malformed stability attribute");
538 becoming_feature = None;
539 if line.contains("rustc_const_unstable(") {
540 // `const fn` features are handled specially.
541 let feature_name = match find_attr_val(line, "feature").or_else(|| {
542 iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature"))
545 None => err!("malformed stability attribute: missing `feature` key"),
547 let feature = Feature {
548 level: Status::Unstable,
550 has_gate_test: false,
551 tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
553 mf(Ok((feature_name, feature)), file, i + 1);
556 let level = if line.contains("[unstable(") {
558 } else if line.contains("[stable(") {
563 let feature_name = match find_attr_val(line, "feature")
564 .or_else(|| iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature")))
567 None => err!("malformed stability attribute: missing `feature` key"),
569 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
570 Some(Ok(since)) => Some(since),
572 err!("malformed stability attribute: can't parse `since` key");
574 None if level == Status::Stable => {
575 err!("malformed stability attribute: missing the `since` key");
579 let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
581 let feature = Feature { level, since, has_gate_test: false, tracking_issue };
582 if line.contains(']') {
583 mf(Ok((feature_name, feature)), file, i + 1);
585 becoming_feature = Some((feature_name, feature));