diff --git a/Cargo.lock b/Cargo.lock index 40b8cf507e97c..657831be894f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3801,9 +3801,11 @@ dependencies = [ name = "tidy" version = "0.1.0" dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/src/bootstrap/test.rs b/src/bootstrap/test.rs index 74caaae2840c5..2f9bd067c3115 100644 --- a/src/bootstrap/test.rs +++ b/src/bootstrap/test.rs @@ -709,8 +709,8 @@ impl Step for Tidy { if !builder.config.vendor { cmd.arg("--no-vendor"); } - if !builder.config.verbose_tests { - cmd.arg("--quiet"); + if builder.is_verbose() { + cmd.arg("--verbose"); } let _folder = builder.fold_output(|| "tidy"); diff --git a/src/tools/tidy/Cargo.toml b/src/tools/tidy/Cargo.toml index eeac6cfbb30e0..43cae31f33f1f 100644 --- a/src/tools/tidy/Cargo.toml +++ b/src/tools/tidy/Cargo.toml @@ -8,3 +8,5 @@ edition = "2018" regex = "1" serde = { version = "1.0.8", features = ["derive"] } serde_json = "1.0.2" +lazy_static = "1" +walkdir = "2" diff --git a/src/tools/tidy/src/bins.rs b/src/tools/tidy/src/bins.rs index 610d1d8af3b5e..680585a6e04a7 100644 --- a/src/tools/tidy/src/bins.rs +++ b/src/tools/tidy/src/bins.rs @@ -25,16 +25,17 @@ pub fn check(path: &Path, bad: &mut bool) { } } - super::walk(path, + super::walk_no_read(path, &mut |path| super::filter_dirs(path) || path.ends_with("src/etc"), - &mut |file| { + &mut |entry| { + let file = entry.path(); let filename = file.file_name().unwrap().to_string_lossy(); let extensions = [".py", ".sh"]; if extensions.iter().any(|e| filename.ends_with(e)) { return; } - let metadata = t!(fs::symlink_metadata(&file), &file); + let metadata = t!(entry.metadata(), file); if metadata.mode() & 0o111 != 0 { let rel_path = file.strip_prefix(path).unwrap(); let git_friendly_path = rel_path.to_str().unwrap().replace("\\", "/"); diff --git a/src/tools/tidy/src/errors.rs b/src/tools/tidy/src/errors.rs index ef1000ee5065a..1bc27745376cc 100644 --- a/src/tools/tidy/src/errors.rs +++ b/src/tools/tidy/src/errors.rs @@ -4,24 +4,19 @@ //! statistics about the error codes. use std::collections::HashMap; -use std::fs::File; -use std::io::prelude::*; use std::path::Path; pub fn check(path: &Path, bad: &mut bool) { - let mut contents = String::new(); let mut map: HashMap<_, Vec<_>> = HashMap::new(); super::walk(path, &mut |path| super::filter_dirs(path) || path.ends_with("src/test"), - &mut |file| { + &mut |entry, contents| { + let file = entry.path(); let filename = file.file_name().unwrap().to_string_lossy(); if filename != "error_codes.rs" { return } - contents.truncate(0); - t!(t!(File::open(file)).read_to_string(&mut contents)); - // In the `register_long_diagnostics!` macro, entries look like this: // // ``` diff --git a/src/tools/tidy/src/features.rs b/src/tools/tidy/src/features.rs index 637f10c5ae745..1841beb1fd116 100644 --- a/src/tools/tidy/src/features.rs +++ b/src/tools/tidy/src/features.rs @@ -11,11 +11,10 @@ use std::collections::HashMap; use std::fmt; -use std::fs::{self, File}; -use std::io::prelude::*; +use std::fs; use std::path::Path; -use regex::{Regex, escape}; +use regex::Regex; mod version; use version::Version; @@ -51,20 +50,48 @@ pub struct Feature { pub type Features = HashMap; -pub fn check(path: &Path, bad: &mut bool, quiet: bool) { +pub struct CollectedFeatures { + pub lib: Features, + pub lang: Features, +} + +// Currently only used for unstable book generation +pub fn collect_lib_features(base_src_path: &Path) -> Features { + let mut lib_features = Features::new(); + + // This library feature is defined in the `compiler_builtins` crate, which + // has been moved out-of-tree. Now it can no longer be auto-discovered by + // `tidy`, because we need to filter out its (submodule) directory. Manually + // add it to the set of known library features so we can still generate docs. + lib_features.insert("compiler_builtins_lib".to_owned(), Feature { + level: Status::Unstable, + since: None, + has_gate_test: false, + tracking_issue: None, + }); + + map_lib_features(base_src_path, + &mut |res, _, _| { + if let Ok((name, feature)) = res { + lib_features.insert(name.to_owned(), feature); + } + }); + lib_features +} + +pub fn check(path: &Path, bad: &mut bool, verbose: bool) -> CollectedFeatures { let mut features = collect_lang_features(path, bad); assert!(!features.is_empty()); let lib_features = get_and_check_lib_features(path, bad, &features); assert!(!lib_features.is_empty()); - let mut contents = String::new(); - super::walk_many(&[&path.join("test/ui"), &path.join("test/ui-fulldeps"), &path.join("test/compile-fail")], &mut |path| super::filter_dirs(path), - &mut |file| { + &mut |entry, contents| { + let file = entry.path(); let filename = file.file_name().unwrap().to_string_lossy(); if !filename.ends_with(".rs") || filename == "features.rs" || filename == "diagnostic_list.rs" { @@ -74,9 +101,6 @@ pub fn check(path: &Path, bad: &mut bool, quiet: bool) { let filen_underscore = filename.replace('-',"_").replace(".rs",""); let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features); - contents.truncate(0); - t!(t!(File::open(&file), &file).read_to_string(&mut contents)); - for (i, line) in contents.lines().enumerate() { let mut err = |msg: &str| { tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg); @@ -130,21 +154,23 @@ pub fn check(path: &Path, bad: &mut bool, quiet: bool) { } if *bad { - return; - } - if quiet { - println!("* {} features", features.len()); - return; + return CollectedFeatures { lib: lib_features, lang: features }; } - let mut lines = Vec::new(); - lines.extend(format_features(&features, "lang")); - lines.extend(format_features(&lib_features, "lib")); + if verbose { + let mut lines = Vec::new(); + lines.extend(format_features(&features, "lang")); + lines.extend(format_features(&lib_features, "lib")); - lines.sort(); - for line in lines { - println!("* {}", line); + lines.sort(); + for line in lines { + println!("* {}", line); + } + } else { + println!("* {} features", features.len()); } + + CollectedFeatures { lib: lib_features, lang: features } } fn format_features<'a>(features: &'a Features, family: &'a str) -> impl Iterator + 'a { @@ -159,8 +185,19 @@ fn format_features<'a>(features: &'a Features, family: &'a str) -> impl Iterator } fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> { - let r = Regex::new(&format!(r#"{}\s*=\s*"([^"]*)""#, escape(attr))) - .expect("malformed regex for find_attr_val"); + lazy_static::lazy_static! { + static ref ISSUE: Regex = Regex::new(r#"issue\s*=\s*"([^"]*)""#).unwrap(); + static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap(); + static ref SINCE: Regex = Regex::new(r#"since\s*=\s*"([^"]*)""#).unwrap(); + } + + let r = match attr { + "issue" => &*ISSUE, + "feature" => &*FEATURE, + "since" => &*SINCE, + _ => unimplemented!("{} not handled", attr), + }; + r.captures(line) .and_then(|c| c.get(1)) .map(|m| m.as_str()) @@ -175,9 +212,11 @@ fn test_find_attr_val() { } fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool { - if filen_underscore.starts_with("feature_gate") { + let prefix = "feature_gate_"; + if filen_underscore.starts_with(prefix) { for (n, f) in features.iter_mut() { - if filen_underscore == format!("feature_gate_{}", n) { + // Equivalent to filen_underscore == format!("feature_gate_{}", n) + if &filen_underscore[prefix.len()..] == n { f.has_gate_test = true; return true; } @@ -295,32 +334,6 @@ pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features { .collect() } -pub fn collect_lib_features(base_src_path: &Path) -> Features { - let mut lib_features = Features::new(); - - // This library feature is defined in the `compiler_builtins` crate, which - // has been moved out-of-tree. Now it can no longer be auto-discovered by - // `tidy`, because we need to filter out its (submodule) directory. Manually - // add it to the set of known library features so we can still generate docs. - lib_features.insert("compiler_builtins_lib".to_owned(), Feature { - level: Status::Unstable, - since: None, - has_gate_test: false, - tracking_issue: None, - }); - - map_lib_features(base_src_path, - &mut |res, _, _| { - if let Ok((name, feature)) = res { - if lib_features.contains_key(name) { - return; - } - lib_features.insert(name.to_owned(), feature); - } - }); - lib_features -} - fn get_and_check_lib_features(base_src_path: &Path, bad: &mut bool, lang_features: &Features) -> Features { @@ -355,20 +368,25 @@ fn get_and_check_lib_features(base_src_path: &Path, fn map_lib_features(base_src_path: &Path, mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) { - let mut contents = String::new(); super::walk(base_src_path, &mut |path| super::filter_dirs(path) || path.ends_with("src/test"), - &mut |file| { + &mut |entry, contents| { + let file = entry.path(); let filename = file.file_name().unwrap().to_string_lossy(); if !filename.ends_with(".rs") || filename == "features.rs" || filename == "diagnostic_list.rs" { return; } - contents.truncate(0); - t!(t!(File::open(&file), &file).read_to_string(&mut contents)); + // This is an early exit -- all the attributes we're concerned with must contain this: + // * rustc_const_unstable( + // * unstable( + // * stable( + if !contents.contains("stable(") { + return; + } - let mut becoming_feature: Option<(String, Feature)> = None; + let mut becoming_feature: Option<(&str, Feature)> = None; for (i, line) in contents.lines().enumerate() { macro_rules! err { ($msg:expr) => {{ @@ -447,7 +465,7 @@ fn map_lib_features(base_src_path: &Path, if line.contains(']') { mf(Ok((feature_name, feature)), file, i + 1); } else { - becoming_feature = Some((feature_name.to_owned(), feature)); + becoming_feature = Some((feature_name, feature)); } } }); diff --git a/src/tools/tidy/src/lib.rs b/src/tools/tidy/src/lib.rs index d06c99725bc6a..a0bf0b0735418 100644 --- a/src/tools/tidy/src/lib.rs +++ b/src/tools/tidy/src/lib.rs @@ -3,7 +3,9 @@ //! This library contains the tidy lints and exposes it //! to be used by tools. -use std::fs; +use walkdir::{DirEntry, WalkDir}; +use std::fs::File; +use std::io::Read; use std::path::Path; @@ -65,25 +67,35 @@ fn filter_dirs(path: &Path) -> bool { skip.iter().any(|p| path.ends_with(p)) } -fn walk_many(paths: &[&Path], skip: &mut dyn FnMut(&Path) -> bool, f: &mut dyn FnMut(&Path)) { + +fn walk_many( + paths: &[&Path], skip: &mut dyn FnMut(&Path) -> bool, f: &mut dyn FnMut(&DirEntry, &str) +) { for path in paths { walk(path, skip, f); } } -fn walk(path: &Path, skip: &mut dyn FnMut(&Path) -> bool, f: &mut dyn FnMut(&Path)) { - if let Ok(dir) = fs::read_dir(path) { - for entry in dir { - let entry = t!(entry); - let kind = t!(entry.file_type()); - let path = entry.path(); - if kind.is_dir() { - if !skip(&path) { - walk(&path, skip, f); - } - } else { - f(&path); +fn walk(path: &Path, skip: &mut dyn FnMut(&Path) -> bool, f: &mut dyn FnMut(&DirEntry, &str)) { + let mut contents = String::new(); + walk_no_read(path, skip, &mut |entry| { + contents.clear(); + if t!(File::open(entry.path()), entry.path()).read_to_string(&mut contents).is_err() { + contents.clear(); + } + f(&entry, &contents); + }); +} + +fn walk_no_read(path: &Path, skip: &mut dyn FnMut(&Path) -> bool, f: &mut dyn FnMut(&DirEntry)) { + let walker = WalkDir::new(path).into_iter() + .filter_entry(|e| !skip(e.path())); + for entry in walker { + if let Ok(entry) = entry { + if entry.file_type().is_dir() { + continue; } + f(&entry); } } } diff --git a/src/tools/tidy/src/libcoretest.rs b/src/tools/tidy/src/libcoretest.rs index b15b9c3462f79..ea92f989ada7d 100644 --- a/src/tools/tidy/src/libcoretest.rs +++ b/src/tools/tidy/src/libcoretest.rs @@ -4,29 +4,22 @@ //! item. All tests must be written externally in `libcore/tests`. use std::path::Path; -use std::fs::read_to_string; pub fn check(path: &Path, bad: &mut bool) { let libcore_path = path.join("libcore"); super::walk( &libcore_path, &mut |subpath| t!(subpath.strip_prefix(&libcore_path)).starts_with("tests"), - &mut |subpath| { + &mut |entry, contents| { + let subpath = entry.path(); if let Some("rs") = subpath.extension().and_then(|e| e.to_str()) { - match read_to_string(subpath) { - Ok(contents) => { - if contents.contains("#[test]") { - tidy_error!( - bad, - "{} contains #[test]; libcore tests must be placed inside \ - `src/libcore/tests/`", - subpath.display() - ); - } - } - Err(err) => { - panic!("failed to read file {:?}: {}", subpath, err); - } + if contents.contains("#[test]") { + tidy_error!( + bad, + "{} contains #[test]; libcore tests must be placed inside \ + `src/libcore/tests/`", + subpath.display() + ); } } }, diff --git a/src/tools/tidy/src/main.rs b/src/tools/tidy/src/main.rs index eef3719043825..918762ed6e69a 100644 --- a/src/tools/tidy/src/main.rs +++ b/src/tools/tidy/src/main.rs @@ -19,14 +19,14 @@ fn main() { let args: Vec = env::args().skip(1).collect(); let mut bad = false; - let quiet = args.iter().any(|s| *s == "--quiet"); + let verbose = args.iter().any(|s| *s == "--verbose"); bins::check(&path, &mut bad); style::check(&path, &mut bad); errors::check(&path, &mut bad); cargo::check(&path, &mut bad); - features::check(&path, &mut bad, quiet); + let collected = features::check(&path, &mut bad, verbose); pal::check(&path, &mut bad); - unstable_book::check(&path, &mut bad); + unstable_book::check(&path, collected, &mut bad); libcoretest::check(&path, &mut bad); if !args.iter().any(|s| *s == "--no-vendor") { deps::check(&path, &mut bad); diff --git a/src/tools/tidy/src/pal.rs b/src/tools/tidy/src/pal.rs index d4a6cf73bf98c..c6bb16318b6ee 100644 --- a/src/tools/tidy/src/pal.rs +++ b/src/tools/tidy/src/pal.rs @@ -31,8 +31,6 @@ //! platform-specific cfgs are allowed. Not sure yet how to deal with //! this in the long term. -use std::fs::File; -use std::io::Read; use std::path::Path; use std::iter::Iterator; @@ -87,29 +85,26 @@ const EXCEPTION_PATHS: &[&str] = &[ ]; pub fn check(path: &Path, bad: &mut bool) { - let mut contents = String::new(); // Sanity check that the complex parsing here works. let mut saw_target_arch = false; let mut saw_cfg_bang = false; - super::walk(path, &mut super::filter_dirs, &mut |file| { + super::walk(path, &mut super::filter_dirs, &mut |entry, contents| { + let file = entry.path(); let filestr = file.to_string_lossy().replace("\\", "/"); if !filestr.ends_with(".rs") { return } let is_exception_path = EXCEPTION_PATHS.iter().any(|s| filestr.contains(&**s)); if is_exception_path { return } - check_cfgs(&mut contents, &file, bad, &mut saw_target_arch, &mut saw_cfg_bang); + check_cfgs(contents, &file, bad, &mut saw_target_arch, &mut saw_cfg_bang); }); assert!(saw_target_arch); assert!(saw_cfg_bang); } -fn check_cfgs(contents: &mut String, file: &Path, +fn check_cfgs(contents: &str, file: &Path, bad: &mut bool, saw_target_arch: &mut bool, saw_cfg_bang: &mut bool) { - contents.truncate(0); - t!(t!(File::open(file), file).read_to_string(contents)); - // For now it's ok to have platform-specific code after 'mod tests'. let mod_tests_idx = find_test_mod(contents); let contents = &contents[..mod_tests_idx]; diff --git a/src/tools/tidy/src/style.rs b/src/tools/tidy/src/style.rs index e860f2e9df0ad..4a159d926b7cc 100644 --- a/src/tools/tidy/src/style.rs +++ b/src/tools/tidy/src/style.rs @@ -13,8 +13,6 @@ //! A number of these checks can be opted-out of with various directives of the form: //! `// ignore-tidy-CHECK-NAME`. -use std::fs::File; -use std::io::prelude::*; use std::path::Path; const COLS: usize = 100; @@ -109,7 +107,11 @@ enum Directive { Ignore(bool), } -fn contains_ignore_directive(contents: &String, check: &str) -> Directive { +fn contains_ignore_directive(can_contain: bool, contents: &str, check: &str) -> Directive { + if !can_contain { + return Directive::Deny; + } + // Update `can_contain` when changing this if contents.contains(&format!("// ignore-tidy-{}", check)) || contents.contains(&format!("# ignore-tidy-{}", check)) { Directive::Ignore(false) @@ -129,8 +131,8 @@ macro_rules! suppressible_tidy_err { } pub fn check(path: &Path, bad: &mut bool) { - let mut contents = String::new(); - super::walk(path, &mut super::filter_dirs, &mut |file| { + super::walk(path, &mut super::filter_dirs, &mut |entry, contents| { + let file = entry.path(); let filename = file.file_name().unwrap().to_string_lossy(); let extensions = [".rs", ".py", ".js", ".sh", ".c", ".cpp", ".h"]; if extensions.iter().all(|e| !filename.ends_with(e)) || @@ -138,19 +140,19 @@ pub fn check(path: &Path, bad: &mut bool) { return } - contents.truncate(0); - t!(t!(File::open(file), file).read_to_string(&mut contents)); - if contents.is_empty() { tidy_error!(bad, "{}: empty file", file.display()); } - let mut skip_cr = contains_ignore_directive(&contents, "cr"); - let mut skip_tab = contains_ignore_directive(&contents, "tab"); - let mut skip_line_length = contains_ignore_directive(&contents, "linelength"); - let mut skip_file_length = contains_ignore_directive(&contents, "filelength"); - let mut skip_end_whitespace = contains_ignore_directive(&contents, "end-whitespace"); - let mut skip_copyright = contains_ignore_directive(&contents, "copyright"); + let can_contain = contents.contains("// ignore-tidy-") || + contents.contains("# ignore-tidy-"); + let mut skip_cr = contains_ignore_directive(can_contain, &contents, "cr"); + let mut skip_tab = contains_ignore_directive(can_contain, &contents, "tab"); + let mut skip_line_length = contains_ignore_directive(can_contain, &contents, "linelength"); + let mut skip_file_length = contains_ignore_directive(can_contain, &contents, "filelength"); + let mut skip_end_whitespace = + contains_ignore_directive(can_contain, &contents, "end-whitespace"); + let mut skip_copyright = contains_ignore_directive(can_contain, &contents, "copyright"); let mut leading_new_lines = false; let mut trailing_new_lines = 0; let mut lines = 0; diff --git a/src/tools/tidy/src/ui_tests.rs b/src/tools/tidy/src/ui_tests.rs index b572b52ea8f35..2c52cecccb5df 100644 --- a/src/tools/tidy/src/ui_tests.rs +++ b/src/tools/tidy/src/ui_tests.rs @@ -4,10 +4,9 @@ use std::fs; use std::path::Path; pub fn check(path: &Path, bad: &mut bool) { - super::walk_many( - &[&path.join("test/ui"), &path.join("test/ui-fulldeps")], - &mut |_| false, - &mut |file_path| { + for path in &[&path.join("test/ui"), &path.join("test/ui-fulldeps")] { + super::walk_no_read(path, &mut |_| false, &mut |entry| { + let file_path = entry.path(); if let Some(ext) = file_path.extension() { if ext == "stderr" || ext == "stdout" { // Test output filenames have one of the formats: @@ -45,6 +44,6 @@ pub fn check(path: &Path, bad: &mut bool) { } } } - }, - ); + }); + } } diff --git a/src/tools/tidy/src/unstable_book.rs b/src/tools/tidy/src/unstable_book.rs index f7e40ce4bae36..fb63520f0684a 100644 --- a/src/tools/tidy/src/unstable_book.rs +++ b/src/tools/tidy/src/unstable_book.rs @@ -1,7 +1,7 @@ use std::collections::BTreeSet; use std::fs; -use std::path; -use crate::features::{collect_lang_features, collect_lib_features, Features, Status}; +use std::path::{PathBuf, Path}; +use crate::features::{CollectedFeatures, Features, Feature, Status}; pub const PATH_STR: &str = "doc/unstable-book"; @@ -12,19 +12,19 @@ pub const LANG_FEATURES_DIR: &str = "src/language-features"; pub const LIB_FEATURES_DIR: &str = "src/library-features"; /// Builds the path to the Unstable Book source directory from the Rust 'src' directory. -pub fn unstable_book_path(base_src_path: &path::Path) -> path::PathBuf { +pub fn unstable_book_path(base_src_path: &Path) -> PathBuf { base_src_path.join(PATH_STR) } /// Builds the path to the directory where the features are documented within the Unstable Book /// source directory. -pub fn unstable_book_lang_features_path(base_src_path: &path::Path) -> path::PathBuf { +pub fn unstable_book_lang_features_path(base_src_path: &Path) -> PathBuf { unstable_book_path(base_src_path).join(LANG_FEATURES_DIR) } /// Builds the path to the directory where the features are documented within the Unstable Book /// source directory. -pub fn unstable_book_lib_features_path(base_src_path: &path::Path) -> path::PathBuf { +pub fn unstable_book_lib_features_path(base_src_path: &Path) -> PathBuf { unstable_book_path(base_src_path).join(LIB_FEATURES_DIR) } @@ -45,7 +45,7 @@ pub fn collect_unstable_feature_names(features: &Features) -> BTreeSet { .collect() } -pub fn collect_unstable_book_section_file_names(dir: &path::Path) -> BTreeSet { +pub fn collect_unstable_book_section_file_names(dir: &Path) -> BTreeSet { fs::read_dir(dir) .expect("could not read directory") .map(|entry| entry.expect("could not read directory entry")) @@ -60,7 +60,7 @@ pub fn collect_unstable_book_section_file_names(dir: &path::Path) -> BTreeSet BTreeSet { collect_unstable_book_section_file_names(&unstable_book_lang_features_path(base_src_path)) } @@ -69,18 +69,26 @@ fn collect_unstable_book_lang_features_section_file_names(base_src_path: &path:: /// /// * hyphens replaced by underscores, /// * the markdown suffix ('.md') removed. -fn collect_unstable_book_lib_features_section_file_names(base_src_path: &path::Path) - -> BTreeSet { +fn collect_unstable_book_lib_features_section_file_names(base_src_path: &Path) -> BTreeSet { collect_unstable_book_section_file_names(&unstable_book_lib_features_path(base_src_path)) } -pub fn check(path: &path::Path, bad: &mut bool) { - // Library features - - let lang_features = collect_lang_features(path, bad); - let lib_features = collect_lib_features(path).into_iter().filter(|&(ref name, _)| { +pub fn check(path: &Path, features: CollectedFeatures, bad: &mut bool) { + let lang_features = features.lang; + let mut lib_features = features.lib.into_iter().filter(|&(ref name, _)| { !lang_features.contains_key(name) - }).collect(); + }).collect::(); + + // This library feature is defined in the `compiler_builtins` crate, which + // has been moved out-of-tree. Now it can no longer be auto-discovered by + // `tidy`, because we need to filter out its (submodule) directory. Manually + // add it to the set of known library features so we can still generate docs. + lib_features.insert("compiler_builtins_lib".to_owned(), Feature { + level: Status::Unstable, + since: None, + has_gate_test: false, + tracking_issue: None, + }); // Library features let unstable_lib_feature_names = collect_unstable_feature_names(&lib_features);