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 std
::collections
::HashMap
;
15 use std
::num
::NonZeroU32
;
26 const FEATURE_GROUP_START_PREFIX
: &str = "// feature-group-start";
27 const FEATURE_GROUP_END_PREFIX
: &str = "// feature-group-end";
29 #[derive(Debug, PartialEq, Clone)]
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",
43 fmt
::Display
::fmt(as_str
, f
)
47 #[derive(Debug, Clone)]
50 pub since
: Option
<Version
>,
51 pub has_gate_test
: bool
,
52 pub tracking_issue
: Option
<NonZeroU32
>,
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(),
63 pub type Features
= HashMap
<String
, Feature
>;
65 pub struct CollectedFeatures
{
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();
74 map_lib_features(base_src_path
, &mut |res
, _
, _
| {
75 if let Ok((name
, feature
)) = res
{
76 lib_features
.insert(name
.to_owned(), feature
);
88 ) -> CollectedFeatures
{
89 let mut features
= collect_lang_features(compiler_path
, bad
);
90 assert
!(!features
.is_empty());
92 let lib_features
= get_and_check_lib_features(lib_path
, bad
, &features
);
93 assert
!(!lib_features
.is_empty());
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"),
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"
113 let filen_underscore
= filename
.replace('
-'
, "_").replace(".rs", "");
114 let filename_is_gate_test
= test_filen_gate(&filen_underscore
, &mut features
);
116 for (i
, line
) in contents
.lines().enumerate() {
117 let mut err
= |msg
: &str| {
118 tidy_error
!(bad
, "{}:{}: {}", file
.display(), i
+ 1, msg
);
121 let gate_test_str
= "gate-test-";
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(),
128 match features
.get_mut(feature_name
) {
130 if filename_is_gate_test
{
132 "The file is already marked as gate test \
133 through its name, no need for a \
134 'gate-test-{}' comment",
138 f
.has_gate_test
= true;
142 "gate-test test found referencing a nonexistent feature '{}'",
151 // Only check the number of lang features.
152 // Obligatory testing for library features is dumb.
153 let gate_untested
= features
155 .filter(|&(_
, f
)| f
.level
== Status
::Unstable
)
156 .filter(|&(_
, f
)| !f
.has_gate_test
)
157 .collect
::<Vec
<_
>>();
159 for &(name
, _
) in gate_untested
.iter() {
160 println
!("Expected a gate test for the feature '{name}'.");
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({})]`.",
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.",
174 if !gate_untested
.is_empty() {
175 tidy_error
!(bad
, "Found {} features without a gate test.", gate_untested
.len());
178 let (version
, channel
) = get_version_and_channel(src_path
);
180 let all_features_iter
= features
182 .map(|feat
| (feat
, "lang"))
183 .chain(lib_features
.iter().map(|feat
| (feat
, "lib")));
184 for ((feature_name
, feature
), kind
) in all_features_iter
{
185 let since
= if let Some(since
) = feature
.since { since }
else { continue }
;
186 if since
> version
&& since
!= Version
::CurrentPlaceholder
{
189 "The stabilization version {since} of {kind} feature `{feature_name}` is newer than the current {version}"
192 if channel
== "nightly" && since
== version
{
195 "The stabilization version {since} of {kind} feature `{feature_name}` is written out but should be {}",
196 version
::VERSION_PLACEHOLDER
199 if channel
!= "nightly" && since
== Version
::CurrentPlaceholder
{
202 "The placeholder use of {kind} feature `{feature_name}` is not allowed on the {channel} channel",
208 return CollectedFeatures { lib: lib_features, lang: features }
;
212 let mut lines
= Vec
::new();
213 lines
.extend(format_features(&features
, "lang"));
214 lines
.extend(format_features(&lib_features
, "lib"));
218 println
!("* {line}");
221 println
!("* {} features", features
.len());
224 CollectedFeatures { lib: lib_features, lang: features }
227 fn get_version_and_channel(src_path
: &Path
) -> (Version
, String
) {
228 let version_str
= t
!(std
::fs
::read_to_string(src_path
.join("version")));
229 let version_str
= version_str
.trim();
230 let version
= t
!(std
::str::FromStr
::from_str(&version_str
).map_err(|e
| format
!("{e:?}")));
231 let channel_str
= t
!(std
::fs
::read_to_string(src_path
.join("ci").join("channel")));
232 (version
, channel_str
.trim().to_owned())
235 fn format_features
<'a
>(
236 features
: &'a Features
,
238 ) -> impl Iterator
<Item
= String
> + 'a
{
239 features
.iter().map(move |(name
, feature
)| {
241 "{:<32} {:<8} {:<12} {:<8}",
245 feature
.since
.map_or("None".to_owned(), |since
| since
.to_string())
250 fn find_attr_val
<'a
>(line
: &'a
str, attr
: &str) -> Option
<&'a
str> {
251 lazy_static
::lazy_static
! {
252 static ref ISSUE
: Regex
= Regex
::new(r
#"issue\s*=\s*"([^"]*)""#).unwrap();
253 static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap();
254 static ref SINCE
: Regex
= Regex
::new(r
#"since\s*=\s*"([^"]*)""#).unwrap();
259 "feature" => &*FEATURE,
261 _ => unimplemented!("{attr} not handled"),
264 r.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
267 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
268 let prefix = "feature_gate_";
269 if filen_underscore.starts_with(prefix) {
270 for (n, f) in features.iter_mut() {
271 // Equivalent to filen_underscore == format!("feature_gate_{n}")
272 if &filen_underscore[prefix.len()..] == n {
273 f.has_gate_test = true;
281 pub fn collect_lang_features(base_compiler_path: &Path, bad: &mut bool) -> Features {
282 let mut all = collect_lang_features_in(base_compiler_path, "active.rs", bad);
283 all.extend(collect_lang_features_in(base_compiler_path, "accepted.rs", bad));
284 all.extend(collect_lang_features_in(base_compiler_path, "removed.rs", bad));
288 fn collect_lang_features_in(base: &Path, file: &str, bad: &mut bool) -> Features {
289 let path = base.join("rustc_feature").join("src").join(file);
290 let contents = t!(fs::read_to_string(&path));
292 // We allow rustc-internal features to omit a tracking issue.
293 // To make tidy accept omitting a tracking issue, group the list of features
294 // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
295 let mut next_feature_omits_tracking_issue = false;
297 let mut in_feature_group = false;
298 let mut prev_names = vec![];
303 .filter_map(|(line, line_number)| {
304 let line = line.trim();
306 // Within -start and -end, the tracking issue can be omitted.
308 "// no-tracking-issue-start" => {
309 next_feature_omits_tracking_issue
= true;
312 "// no-tracking-issue-end" => {
313 next_feature_omits_tracking_issue
= false;
319 if line
.starts_with(FEATURE_GROUP_START_PREFIX
) {
320 if in_feature_group
{
324 new feature group is started without ending the previous one",
330 in_feature_group
= true;
333 } else if line
.starts_with(FEATURE_GROUP_END_PREFIX
) {
334 in_feature_group
= false;
339 let mut parts
= line
.split('
,'
);
340 let level
= match parts
.next().map(|l
| l
.trim().trim_start_matches('
('
)) {
341 Some("active") => Status
::Unstable
,
342 Some("incomplete") => Status
::Unstable
,
343 Some("removed") => Status
::Removed
,
344 Some("accepted") => Status
::Stable
,
347 let name
= parts
.next().unwrap().trim();
349 let since_str
= parts
.next().unwrap().trim().trim_matches('
"');
350 let since = match since_str.parse() {
351 Ok(since) => Some(since),
355 "{}
:{}
: failed to parse since
: {}
({:?}
)",
364 if in_feature_group {
365 if prev_names.last() > Some(&name) {
366 // This assumes the user adds the feature name at the end of the list, as we're
367 // not looking ahead.
368 let correct_index = match prev_names.binary_search(&name) {
370 // This only occurs when the feature name has already been declared.
373 "{}
:{}
: duplicate feature {}
",
378 // skip any additional checks for this line
384 let correct_placement = if correct_index == 0 {
385 "at the beginning of the feature group
".to_owned()
386 } else if correct_index == prev_names.len() {
387 // I don't believe this is reachable given the above assumption, but it
388 // doesn't hurt to be safe.
389 "at the end of the feature group
".to_owned()
393 prev_names[correct_index - 1],
394 prev_names[correct_index],
400 "{}
:{}
: feature {} is not sorted by feature
name (should be {}
)",
407 prev_names.push(name);
410 let issue_str = parts.next().unwrap().trim();
411 let tracking_issue = if issue_str.starts_with("None
") {
412 if level == Status::Unstable && !next_feature_omits_tracking_issue {
415 "{}
:{}
: no tracking issue
for feature {}
",
423 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
424 Some(s.parse().unwrap())
426 Some((name.to_owned(), Feature { level, since, has_gate_test: false, tracking_issue }))
431 fn get_and_check_lib_features(
432 base_src_path: &Path,
434 lang_features: &Features,
436 let mut lib_features = Features::new();
437 map_lib_features(base_src_path, &mut |res, file, line| match res {
439 let mut check_features = |f: &Feature, list: &Features, display: &str| {
440 if let Some(ref s) = list.get(name) {
441 if f.tracking_issue != s.tracking_issue && f.level != Status::Stable {
444 "{}
:{}
: `issue`
\"{}
\" mismatches the {} `issue` of
\"{}
\"",
447 f.tracking_issue_display(),
449 s.tracking_issue_display(),
454 check_features(&f, &lang_features, "corresponding lang feature
");
455 check_features(&f, &lib_features, "previous
");
456 lib_features.insert(name.to_owned(), f);
459 tidy_error!(bad, "{}
:{}
: {}
", file.display(), line, msg);
466 base_src_path: &Path,
467 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize),
471 &mut |path| super::filter_dirs(path) || path.ends_with("src
/test
"),
472 &mut |entry, contents| {
473 let file = entry.path();
474 let filename = file.file_name().unwrap().to_string_lossy();
475 if !filename.ends_with(".rs
")
476 || filename == "features
.rs
"
477 || filename == "diagnostic_list
.rs
"
478 || filename == "error_codes
.rs
"
483 // This is an early exit -- all the attributes we're concerned with must contain this:
484 // * rustc_const_unstable(
487 if !contents.contains("stable(") {
491 let handle_issue_none = |s| match s {
494 let n = issue.parse().expect("issue number is not a valid integer
");
495 assert_ne!(n, 0, "\"none
\" should be used when there is no issue
, not
\"0\"");
499 let mut becoming_feature: Option<(&str, Feature)> = None;
500 let mut iter_lines = contents.lines().enumerate().peekable();
501 while let Some((i, line)) = iter_lines.next() {
504 mf(Err($msg), file, i + 1);
509 lazy_static::lazy_static! {
510 static ref COMMENT_LINE: Regex = Regex::new(r"^
\s*/
/").unwrap();
512 // exclude commented out lines
513 if COMMENT_LINE.is_match(line) {
517 if let Some((ref name, ref mut f)) = becoming_feature {
518 if f.tracking_issue.is_none() {
519 f.tracking_issue = find_attr_val(line, "issue
").and_then(handle_issue_none);
521 if line.ends_with(']') {
522 mf(Ok((name, f.clone())), file, i + 1);
523 } else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"'
)
525 // We need to bail here because we might have missed the
526 // end of a stability attribute above because the ']'
527 // might not have been at the end of the line.
528 // We could then get into the very unfortunate situation that
529 // we continue parsing the file assuming the current stability
530 // attribute has not ended, and ignoring possible feature
531 // attributes in the process.
532 err
!("malformed stability attribute");
537 becoming_feature
= None
;
538 if line
.contains("rustc_const_unstable(") {
539 // `const fn` features are handled specially.
540 let feature_name
= match find_attr_val(line
, "feature") {
542 None
=> err
!("malformed stability attribute: missing `feature` key"),
544 let feature
= Feature
{
545 level
: Status
::Unstable
,
547 has_gate_test
: false,
548 tracking_issue
: find_attr_val(line
, "issue").and_then(handle_issue_none
),
550 mf(Ok((feature_name
, feature
)), file
, i
+ 1);
553 let level
= if line
.contains("[unstable(") {
555 } else if line
.contains("[stable(") {
560 let feature_name
= match find_attr_val(line
, "feature")
561 .or_else(|| iter_lines
.peek().and_then(|next
| find_attr_val(next
.1, "feature")))
564 None
=> err
!("malformed stability attribute: missing `feature` key"),
566 let since
= match find_attr_val(line
, "since").map(|x
| x
.parse()) {
567 Some(Ok(since
)) => Some(since
),
569 err
!("malformed stability attribute: can't parse `since` key");
571 None
if level
== Status
::Stable
=> {
572 err
!("malformed stability attribute: missing the `since` key");
576 let tracking_issue
= find_attr_val(line
, "issue").and_then(handle_issue_none
);
578 let feature
= Feature { level, since, has_gate_test: false, tracking_issue }
;
579 if line
.contains('
]'
) {
580 mf(Ok((feature_name
, feature
)), file
, i
+ 1);
582 becoming_feature
= Some((feature_name
, feature
));