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.
11 use std
::collections
::HashMap
;
13 use std
::fs
::{self, File}
;
14 use std
::io
::prelude
::*;
17 #[derive(Debug, PartialEq, Clone)]
24 impl fmt
::Display
for Status
{
25 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
26 let as_str
= match *self {
27 Status
::Stable
=> "stable",
28 Status
::Unstable
=> "unstable",
29 Status
::Removed
=> "removed",
31 fmt
::Display
::fmt(as_str
, f
)
35 #[derive(Debug, Clone)]
39 pub has_gate_test
: bool
,
40 pub tracking_issue
: Option
<u32>,
43 pub type Features
= HashMap
<String
, Feature
>;
45 pub fn check(path
: &Path
, bad
: &mut bool
, quiet
: bool
) {
46 let mut features
= collect_lang_features(path
, bad
);
47 assert
!(!features
.is_empty());
49 let lib_features
= get_and_check_lib_features(path
, bad
, &features
);
50 assert
!(!lib_features
.is_empty());
52 let mut contents
= String
::new();
54 super::walk_many(&[&path
.join("test/ui"),
55 &path
.join("test/ui-fulldeps"),
56 &path
.join("test/compile-fail")],
57 &mut |path
| super::filter_dirs(path
),
59 let filename
= file
.file_name().unwrap().to_string_lossy();
60 if !filename
.ends_with(".rs") || filename
== "features.rs" ||
61 filename
== "diagnostic_list.rs" {
65 let filen_underscore
= filename
.replace('
-'
,"_").replace(".rs","");
66 let filename_is_gate_test
= test_filen_gate(&filen_underscore
, &mut features
);
69 t
!(t
!(File
::open(&file
), &file
).read_to_string(&mut contents
));
71 for (i
, line
) in contents
.lines().enumerate() {
72 let mut err
= |msg
: &str| {
73 tidy_error
!(bad
, "{}:{}: {}", file
.display(), i
+ 1, msg
);
76 let gate_test_str
= "gate-test-";
78 let feature_name
= match line
.find(gate_test_str
) {
80 line
[i
+gate_test_str
.len()..].splitn(2, ' '
).next().unwrap()
84 match features
.get_mut(feature_name
) {
86 if filename_is_gate_test
{
87 err(&format
!("The file is already marked as gate test \
88 through its name, no need for a \
89 'gate-test-{}' comment",
92 f
.has_gate_test
= true;
95 err(&format
!("gate-test test found referencing a nonexistent feature '{}'",
102 // Only check the number of lang features.
103 // Obligatory testing for library features is dumb.
104 let gate_untested
= features
.iter()
105 .filter(|&(_
, f
)| f
.level
== Status
::Unstable
)
106 .filter(|&(_
, f
)| !f
.has_gate_test
)
107 .collect
::<Vec
<_
>>();
109 for &(name
, _
) in gate_untested
.iter() {
110 println
!("Expected a gate test for the feature '{}'.", name
);
111 println
!("Hint: create a failing test file named 'feature-gate-{}.rs'\
112 \n in the 'ui' test suite, with its failures due to\
113 \n missing usage of #![feature({})].", name
, name
);
114 println
!("Hint: If you already have such a test and don't want to rename it,\
115 \n you can also add a // gate-test-{} line to the test file.",
119 if !gate_untested
.is_empty() {
120 tidy_error
!(bad
, "Found {} features without a gate test.", gate_untested
.len());
127 println
!("* {} features", features
.len());
131 let mut lines
= Vec
::new();
132 for (name
, feature
) in features
.iter() {
133 lines
.push(format
!("{:<32} {:<8} {:<12} {:<8}",
139 for (name
, feature
) in lib_features
{
140 lines
.push(format
!("{:<32} {:<8} {:<12} {:<8}",
149 println
!("* {}", line
);
153 fn find_attr_val
<'a
>(line
: &'a
str, attr
: &str) -> Option
<&'a
str> {
155 .and_then(|i
| line
[i
..].find('
"').map(|j| i + j + 1))
156 .and_then(|i| line[i..].find('"'
).map(|j
| (i
, i
+ j
)))
157 .map(|(i
, j
)| &line
[i
..j
])
160 fn test_filen_gate(filen_underscore
: &str, features
: &mut Features
) -> bool
{
161 if filen_underscore
.starts_with("feature_gate") {
162 for (n
, f
) in features
.iter_mut() {
163 if filen_underscore
== format
!("feature_gate_{}", n
) {
164 f
.has_gate_test
= true;
172 pub fn collect_lang_features(base_src_path
: &Path
, bad
: &mut bool
) -> Features
{
173 let contents
= t
!(fs
::read_to_string(base_src_path
.join("libsyntax/feature_gate.rs")));
175 // We allow rustc-internal features to omit a tracking issue.
176 // These features must be marked with a `// rustc internal` in its own group.
177 let mut next_feature_is_rustc_internal
= false;
179 contents
.lines().zip(1..)
180 .filter_map(|(line
, line_number
)| {
181 let line
= line
.trim();
182 if line
.starts_with("// rustc internal") {
183 next_feature_is_rustc_internal
= true;
185 } else if line
.is_empty() {
186 next_feature_is_rustc_internal
= false;
190 let mut parts
= line
.split('
,'
);
191 let level
= match parts
.next().map(|l
| l
.trim().trim_start_matches('
('
)) {
192 Some("active") => Status
::Unstable
,
193 Some("removed") => Status
::Removed
,
194 Some("accepted") => Status
::Stable
,
197 let name
= parts
.next().unwrap().trim();
198 let since
= parts
.next().unwrap().trim().trim_matches('
"');
199 let issue_str = parts.next().unwrap().trim();
200 let tracking_issue = if issue_str.starts_with("None
") {
201 if level == Status::Unstable && !next_feature_is_rustc_internal {
205 "libsyntax
/feature_gate
.rs
:{}
: no tracking issue
for feature {}
",
212 next_feature_is_rustc_internal = false;
213 let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap();
214 Some(s.parse().unwrap())
216 Some((name.to_owned(),
219 since: since.to_owned(),
220 has_gate_test: false,
227 pub fn collect_lib_features(base_src_path: &Path) -> Features {
228 let mut lib_features = Features::new();
230 // This library feature is defined in the `compiler_builtins` crate, which
231 // has been moved out-of-tree. Now it can no longer be auto-discovered by
232 // `tidy`, because we need to filter out its (submodule) directory. Manually
233 // add it to the set of known library features so we can still generate docs.
234 lib_features.insert("compiler_builtins_lib
".to_owned(), Feature {
235 level: Status::Unstable,
236 since: String::new(),
237 has_gate_test: false,
238 tracking_issue: None,
241 map_lib_features(base_src_path,
243 if let Ok((name, feature)) = res {
244 if lib_features.contains_key(name) {
247 lib_features.insert(name.to_owned(), feature);
253 fn get_and_check_lib_features(base_src_path: &Path,
255 lang_features: &Features) -> Features {
256 let mut lib_features = Features::new();
257 map_lib_features(base_src_path,
258 &mut |res, file, line| {
261 let mut check_features = |f: &Feature, list: &Features, display: &str| {
262 if let Some(ref s) = list.get(name) {
263 if f.tracking_issue != s.tracking_issue {
265 "{}
:{}
: mismatches the `issue`
in {}
",
272 check_features(&f, &lang_features, "corresponding lang feature
");
273 check_features(&f, &lib_features, "previous
");
274 lib_features.insert(name.to_owned(), f);
277 tidy_error!(bad, "{}
:{}
: {}
", file.display(), line, msg);
285 fn map_lib_features(base_src_path: &Path,
286 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) {
287 let mut contents = String::new();
288 super::walk(base_src_path,
289 &mut |path| super::filter_dirs(path) || path.ends_with("src
/test
"),
291 let filename = file.file_name().unwrap().to_string_lossy();
292 if !filename.ends_with(".rs
") || filename == "features
.rs
" ||
293 filename == "diagnostic_list
.rs
" {
297 contents.truncate(0);
298 t!(t!(File::open(&file), &file).read_to_string(&mut contents));
300 let mut becoming_feature: Option<(String, Feature)> = None;
301 for (i, line) in contents.lines().enumerate() {
304 mf(Err($msg), file, i + 1);
308 if let Some((ref name, ref mut f)) = becoming_feature {
309 if f.tracking_issue.is_none() {
310 f.tracking_issue = find_attr_val(line, "issue
")
311 .map(|s| s.parse().unwrap());
313 if line.ends_with(']') {
314 mf(Ok((name, f.clone())), file, i + 1);
315 } else if !line.ends_with(',') && !line.ends_with('\\') {
316 // We need to bail here because we might have missed the
317 // end of a stability attribute above because the ']'
318 // might not have been at the end of the line.
319 // We could then get into the very unfortunate situation that
320 // we continue parsing the file assuming the current stability
321 // attribute has not ended, and ignoring possible feature
322 // attributes in the process.
323 err!("malformed stability attribute
");
328 becoming_feature = None;
329 if line.contains("rustc_const_unstable(") {
330 // `const fn` features are handled specially.
331 let feature_name = match find_attr_val(line, "feature
") {
333 None => err!("malformed stability attribute
"),
335 let feature = Feature {
336 level: Status::Unstable,
337 since: "None
".to_owned(),
338 has_gate_test: false,
339 // FIXME(#57563): #57563 is now used as a common tracking issue,
340 // although we would like to have specific tracking issues for each
341 // `rustc_const_unstable` in the future.
342 tracking_issue: Some(57563),
344 mf(Ok((feature_name, feature)), file, i + 1);
347 let level = if line.contains("[unstable(") {
349 } else if line.contains("[stable(") {
354 let feature_name = match find_attr_val(line, "feature
") {
356 None => err!("malformed stability attribute
"),
358 let since = match find_attr_val(line, "since
") {
360 None if level == Status::Stable => {
361 err!("malformed stability attribute
");
365 let tracking_issue = find_attr_val(line, "issue
").map(|s| s.parse().unwrap());
367 let feature = Feature {
369 since: since.to_owned(),
370 has_gate_test: false,
373 if line.contains(']') {
374 mf(Ok((feature_name, feature)), file, i + 1);
376 becoming_feature = Some((feature_name.to_owned(), feature));