blob: d900c04c124e81a08ec9a00821449d8630bc1871 [file] [log] [blame]
//! Tidy check to ensure that unstable features are all in order.
//!
//! This check will ensure properties like:
//!
//! * All stability attributes look reasonably well formed.
//! * The set of library features is disjoint from the set of language features.
//! * Library features have at most one stability level.
//! * Library features have at most one `since` value.
//! * All unstable lang features have tests to ensure they are actually unstable.
//! * Language features in a group are sorted by feature name.
use crate::walk::{filter_dirs, filter_not_rust, walk, walk_many};
use std::collections::hash_map::{Entry, HashMap};
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::num::NonZeroU32;
use std::path::Path;
use regex::Regex;
#[cfg(test)]
mod tests;
mod version;
use version::Version;
const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
#[derive(Debug, PartialEq, Clone)]
pub enum Status {
Stable,
Removed,
Unstable,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let as_str = match *self {
Status::Stable => "stable",
Status::Unstable => "unstable",
Status::Removed => "removed",
};
fmt::Display::fmt(as_str, f)
}
}
#[derive(Debug, Clone)]
pub struct Feature {
pub level: Status,
pub since: Option<Version>,
pub has_gate_test: bool,
pub tracking_issue: Option<NonZeroU32>,
}
impl Feature {
fn tracking_issue_display(&self) -> impl fmt::Display {
match self.tracking_issue {
None => "none".to_string(),
Some(x) => x.to_string(),
}
}
}
pub type Features = HashMap<String, Feature>;
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();
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(
src_path: &Path,
tests_path: &Path,
compiler_path: &Path,
lib_path: &Path,
bad: &mut bool,
verbose: bool,
) -> CollectedFeatures {
let mut features = collect_lang_features(compiler_path, bad);
assert!(!features.is_empty());
let lib_features = get_and_check_lib_features(lib_path, bad, &features);
assert!(!lib_features.is_empty());
walk_many(
&[
&tests_path.join("ui"),
&tests_path.join("ui-fulldeps"),
&tests_path.join("rustdoc-ui"),
&tests_path.join("rustdoc"),
],
|path, _is_dir| {
filter_dirs(path)
|| filter_not_rust(path)
|| path.file_name() == Some(OsStr::new("features.rs"))
|| path.file_name() == Some(OsStr::new("diagnostic_list.rs"))
},
&mut |entry, contents| {
let file = entry.path();
let filename = file.file_name().unwrap().to_string_lossy();
let filen_underscore = filename.replace('-', "_").replace(".rs", "");
let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
for (i, line) in contents.lines().enumerate() {
let mut err = |msg: &str| {
tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
};
let gate_test_str = "gate-test-";
let feature_name = match line.find(gate_test_str) {
// NB: the `splitn` always succeeds, even if the delimiter is not present.
Some(i) => line[i + gate_test_str.len()..].splitn(2, ' ').next().unwrap(),
None => continue,
};
match features.get_mut(feature_name) {
Some(f) => {
if filename_is_gate_test {
err(&format!(
"The file is already marked as gate test \
through its name, no need for a \
'gate-test-{}' comment",
feature_name
));
}
f.has_gate_test = true;
}
None => {
err(&format!(
"gate-test test found referencing a nonexistent feature '{}'",
feature_name
));
}
}
}
},
);
// Only check the number of lang features.
// Obligatory testing for library features is dumb.
let gate_untested = features
.iter()
.filter(|&(_, f)| f.level == Status::Unstable)
.filter(|&(_, f)| !f.has_gate_test)
.collect::<Vec<_>>();
for &(name, _) in gate_untested.iter() {
println!("Expected a gate test for the feature '{name}'.");
println!(
"Hint: create a failing test file named 'tests/ui/feature-gates/feature-gate-{}.rs',\
\n with its failures due to missing usage of `#![feature({})]`.",
name.replace("_", "-"),
name
);
println!(
"Hint: If you already have such a test and don't want to rename it,\
\n you can also add a // gate-test-{} line to the test file.",
name
);
}
if !gate_untested.is_empty() {
tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
}
let (version, channel) = get_version_and_channel(src_path);
let all_features_iter = features
.iter()
.map(|feat| (feat, "lang"))
.chain(lib_features.iter().map(|feat| (feat, "lib")));
for ((feature_name, feature), kind) in all_features_iter {
let since = if let Some(since) = feature.since { since } else { continue };
if since > version && since != Version::CurrentPlaceholder {
tidy_error!(
bad,
"The stabilization version {since} of {kind} feature `{feature_name}` is newer than the current {version}"
);
}
if channel == "nightly" && since == version {
tidy_error!(
bad,
"The stabilization version {since} of {kind} feature `{feature_name}` is written out but should be {}",
version::VERSION_PLACEHOLDER
);
}
if channel != "nightly" && since == Version::CurrentPlaceholder {
tidy_error!(
bad,
"The placeholder use of {kind} feature `{feature_name}` is not allowed on the {channel} channel",
);
}
}
if *bad {
return CollectedFeatures { lib: lib_features, lang: features };
}
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}");
}
}
CollectedFeatures { lib: lib_features, lang: features }
}
fn get_version_and_channel(src_path: &Path) -> (Version, String) {
let version_str = t!(std::fs::read_to_string(src_path.join("version")));
let version_str = version_str.trim();
let version = t!(std::str::FromStr::from_str(&version_str).map_err(|e| format!("{e:?}")));
let channel_str = t!(std::fs::read_to_string(src_path.join("ci").join("channel")));
(version, channel_str.trim().to_owned())
}
fn format_features<'a>(
features: &'a Features,
family: &'a str,
) -> impl Iterator<Item = String> + 'a {
features.iter().map(move |(name, feature)| {
format!(
"{:<32} {:<8} {:<12} {:<8}",
name,
family,
feature.level,
feature.since.map_or("None".to_owned(), |since| since.to_string())
)
})
}
fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
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!("{attr} not handled"),
};
r.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
}
fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
let prefix = "feature_gate_";
if filen_underscore.starts_with(prefix) {
for (n, f) in features.iter_mut() {
// Equivalent to filen_underscore == format!("feature_gate_{n}")
if &filen_underscore[prefix.len()..] == n {
f.has_gate_test = true;
return true;
}
}
}
false
}
pub fn collect_lang_features(base_compiler_path: &Path, bad: &mut bool) -> Features {
let mut features = Features::new();
collect_lang_features_in(&mut features, base_compiler_path, "active.rs", bad);
collect_lang_features_in(&mut features, base_compiler_path, "accepted.rs", bad);
collect_lang_features_in(&mut features, base_compiler_path, "removed.rs", bad);
features
}
fn collect_lang_features_in(features: &mut Features, base: &Path, file: &str, bad: &mut bool) {
let path = base.join("rustc_feature").join("src").join(file);
let contents = t!(fs::read_to_string(&path));
// We allow rustc-internal features to omit a tracking issue.
// To make tidy accept omitting a tracking issue, group the list of features
// without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
let mut next_feature_omits_tracking_issue = false;
let mut in_feature_group = false;
let mut prev_names = vec![];
let lines = contents.lines().zip(1..);
for (line, line_number) in lines {
let line = line.trim();
// Within -start and -end, the tracking issue can be omitted.
match line {
"// no-tracking-issue-start" => {
next_feature_omits_tracking_issue = true;
continue;
}
"// no-tracking-issue-end" => {
next_feature_omits_tracking_issue = false;
continue;
}
_ => {}
}
if line.starts_with(FEATURE_GROUP_START_PREFIX) {
if in_feature_group {
tidy_error!(
bad,
"{}:{}: \
new feature group is started without ending the previous one",
path.display(),
line_number,
);
}
in_feature_group = true;
prev_names = vec![];
continue;
} else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
in_feature_group = false;
prev_names = vec![];
continue;
}
let mut parts = line.split(',');
let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
Some("active") => Status::Unstable,
Some("incomplete") => Status::Unstable,
Some("internal") => Status::Unstable,
Some("removed") => Status::Removed,
Some("accepted") => Status::Stable,
_ => continue,
};
let name = parts.next().unwrap().trim();
let since_str = parts.next().unwrap().trim().trim_matches('"');
let since = match since_str.parse() {
Ok(since) => Some(since),
Err(err) => {
tidy_error!(
bad,
"{}:{}: failed to parse since: {} ({:?})",
path.display(),
line_number,
since_str,
err,
);
None
}
};
if in_feature_group {
if prev_names.last() > Some(&name) {
// This assumes the user adds the feature name at the end of the list, as we're
// not looking ahead.
let correct_index = match prev_names.binary_search(&name) {
Ok(_) => {
// This only occurs when the feature name has already been declared.
tidy_error!(
bad,
"{}:{}: duplicate feature {}",
path.display(),
line_number,
name,
);
// skip any additional checks for this line
continue;
}
Err(index) => index,
};
let correct_placement = if correct_index == 0 {
"at the beginning of the feature group".to_owned()
} else if correct_index == prev_names.len() {
// I don't believe this is reachable given the above assumption, but it
// doesn't hurt to be safe.
"at the end of the feature group".to_owned()
} else {
format!(
"between {} and {}",
prev_names[correct_index - 1],
prev_names[correct_index],
)
};
tidy_error!(
bad,
"{}:{}: feature {} is not sorted by feature name (should be {})",
path.display(),
line_number,
name,
correct_placement,
);
}
prev_names.push(name);
}
let issue_str = parts.next().unwrap().trim();
let tracking_issue = if issue_str.starts_with("None") {
if level == Status::Unstable && !next_feature_omits_tracking_issue {
tidy_error!(
bad,
"{}:{}: no tracking issue for feature {}",
path.display(),
line_number,
name,
);
}
None
} else {
let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
Some(s.parse().unwrap())
};
match features.entry(name.to_owned()) {
Entry::Occupied(e) => {
tidy_error!(
bad,
"{}:{} feature {name} already specified with status '{}'",
path.display(),
line_number,
e.get().level,
);
}
Entry::Vacant(e) => {
e.insert(Feature { level, since, has_gate_test: false, tracking_issue });
}
}
}
}
fn get_and_check_lib_features(
base_src_path: &Path,
bad: &mut bool,
lang_features: &Features,
) -> Features {
let mut lib_features = Features::new();
map_lib_features(base_src_path, &mut |res, file, line| match res {
Ok((name, f)) => {
let mut check_features = |f: &Feature, list: &Features, display: &str| {
if let Some(ref s) = list.get(name) {
if f.tracking_issue != s.tracking_issue && f.level != Status::Stable {
tidy_error!(
bad,
"{}:{}: `issue` \"{}\" mismatches the {} `issue` of \"{}\"",
file.display(),
line,
f.tracking_issue_display(),
display,
s.tracking_issue_display(),
);
}
}
};
check_features(&f, &lang_features, "corresponding lang feature");
check_features(&f, &lib_features, "previous");
lib_features.insert(name.to_owned(), f);
}
Err(msg) => {
tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
}
});
lib_features
}
fn map_lib_features(
base_src_path: &Path,
mf: &mut (dyn Send + Sync + FnMut(Result<(&str, Feature), &str>, &Path, usize)),
) {
walk(
base_src_path,
|path, _is_dir| filter_dirs(path) || path.ends_with("tests"),
&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"
|| filename == "error_codes.rs"
{
return;
}
// 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 handle_issue_none = |s| match s {
"none" => None,
issue => {
let n = issue.parse().expect("issue number is not a valid integer");
assert_ne!(n, 0, "\"none\" should be used when there is no issue, not \"0\"");
NonZeroU32::new(n)
}
};
let mut becoming_feature: Option<(&str, Feature)> = None;
let mut iter_lines = contents.lines().enumerate().peekable();
while let Some((i, line)) = iter_lines.next() {
macro_rules! err {
($msg:expr) => {{
mf(Err($msg), file, i + 1);
continue;
}};
}
lazy_static::lazy_static! {
static ref COMMENT_LINE: Regex = Regex::new(r"^\s*//").unwrap();
}
// exclude commented out lines
if COMMENT_LINE.is_match(line) {
continue;
}
if let Some((ref name, ref mut f)) = becoming_feature {
if f.tracking_issue.is_none() {
f.tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
}
if line.ends_with(']') {
mf(Ok((name, f.clone())), file, i + 1);
} else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"')
{
// We need to bail here because we might have missed the
// end of a stability attribute above because the ']'
// might not have been at the end of the line.
// We could then get into the very unfortunate situation that
// we continue parsing the file assuming the current stability
// attribute has not ended, and ignoring possible feature
// attributes in the process.
err!("malformed stability attribute");
} else {
continue;
}
}
becoming_feature = None;
if line.contains("rustc_const_unstable(") {
// `const fn` features are handled specially.
let feature_name = match find_attr_val(line, "feature").or_else(|| {
iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature"))
}) {
Some(name) => name,
None => err!("malformed stability attribute: missing `feature` key"),
};
let feature = Feature {
level: Status::Unstable,
since: None,
has_gate_test: false,
tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
};
mf(Ok((feature_name, feature)), file, i + 1);
continue;
}
let level = if line.contains("[unstable(") {
Status::Unstable
} else if line.contains("[stable(") {
Status::Stable
} else {
continue;
};
let feature_name = match find_attr_val(line, "feature")
.or_else(|| iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature")))
{
Some(name) => name,
None => err!("malformed stability attribute: missing `feature` key"),
};
let since = match find_attr_val(line, "since").map(|x| x.parse()) {
Some(Ok(since)) => Some(since),
Some(Err(_err)) => {
err!("malformed stability attribute: can't parse `since` key");
}
None if level == Status::Stable => {
err!("malformed stability attribute: missing the `since` key");
}
None => None,
};
let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
let feature = Feature { level, since, has_gate_test: false, tracking_issue };
if line.contains(']') {
mf(Ok((feature_name, feature)), file, i + 1);
} else {
becoming_feature = Some((feature_name, feature));
}
}
},
);
}