1 // Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
11 //! Tidy check to ensure that unstable features are all in order
13 //! This check will ensure properties like:
15 //! * All stability attributes look reasonably well formed
16 //! * The set of library features is disjoint from the set of language features
17 //! * Library features have at most one stability level
18 //! * Library features have at most one `since` value
19 //! * All unstable lang features have tests to ensure they are actually unstable
21 use std::collections::HashMap;
23 use std::fs::{self, File};
24 use std::io::prelude::*;
27 #[derive(Debug, PartialEq, Clone)]
34 impl fmt::Display for Status {
35 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36 let as_str = match *self {
37 Status::Stable => "stable",
38 Status::Unstable => "unstable",
39 Status::Removed => "removed",
41 fmt::Display::fmt(as_str, f)
45 #[derive(Debug, Clone)]
49 pub has_gate_test: bool,
50 pub tracking_issue: Option<u32>,
53 pub type Features = HashMap<String, Feature>;
55 pub fn check(path: &Path, bad: &mut bool, quiet: bool) {
56 let mut features = collect_lang_features(path, bad);
57 assert!(!features.is_empty());
59 let lib_features = get_and_check_lib_features(path, bad, &features);
60 assert!(!lib_features.is_empty());
62 let mut contents = String::new();
64 super::walk_many(&[&path.join("test/ui-fulldeps"),
65 &path.join("test/ui"),
66 &path.join("test/compile-fail"),
67 &path.join("test/compile-fail-fulldeps"),
68 &path.join("test/parse-fail"),
69 &path.join("test/ui"),],
70 &mut |path| super::filter_dirs(path),
72 let filename = file.file_name().unwrap().to_string_lossy();
73 if !filename.ends_with(".rs") || filename == "features.rs" ||
74 filename == "diagnostic_list.rs" {
78 let filen_underscore = filename.replace('-',"_").replace(".rs","");
79 let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
82 t!(t!(File::open(&file), &file).read_to_string(&mut contents));
84 for (i, line) in contents.lines().enumerate() {
85 let mut err = |msg: &str| {
86 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
89 let gate_test_str = "gate-test-";
91 let feature_name = match line.find(gate_test_str) {
93 line[i+gate_test_str.len()..].splitn(2, ' ').next().unwrap()
97 match features.get_mut(feature_name) {
99 if filename_is_gate_test {
100 err(&format!("The file is already marked as gate test \
101 through its name, no need for a \
102 'gate-test-{}' comment",
105 f.has_gate_test = true;
108 err(&format!("gate-test test found referencing a nonexistent feature '{}'",
115 // Only check the number of lang features.
116 // Obligatory testing for library features is dumb.
117 let gate_untested = features.iter()
118 .filter(|&(_, f)| f.level == Status::Unstable)
119 .filter(|&(_, f)| !f.has_gate_test)
120 .collect::<Vec<_>>();
122 for &(name, _) in gate_untested.iter() {
123 println!("Expected a gate test for the feature '{}'.", name);
124 println!("Hint: create a failing test file named 'feature-gate-{}.rs'\
125 \n in the 'ui' test suite, with its failures due to\
126 \n missing usage of #![feature({})].", name, name);
127 println!("Hint: If you already have such a test and don't want to rename it,\
128 \n you can also add a // gate-test-{} line to the test file.",
132 if !gate_untested.is_empty() {
133 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
140 println!("* {} features", features.len());
144 let mut lines = Vec::new();
145 for (name, feature) in features.iter() {
146 lines.push(format!("{:<32} {:<8} {:<12} {:<8}",
152 for (name, feature) in lib_features {
153 lines.push(format!("{:<32} {:<8} {:<12} {:<8}",
162 println!("* {}", line);
166 fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
168 .and_then(|i| line[i..].find('"').map(|j| i + j + 1))
169 .and_then(|i| line[i..].find('"').map(|j| (i, i + j)))
170 .map(|(i, j)| &line[i..j])
173 fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
174 if filen_underscore.starts_with("feature_gate") {
175 for (n, f) in features.iter_mut() {
176 if filen_underscore == format!("feature_gate_{}", n) {
177 f.has_gate_test = true;
185 pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
186 let contents = t!(fs::read_to_string(base_src_path.join("libsyntax/feature_gate.rs")));
188 // we allow rustc-internal features to omit a tracking issue.
189 // these features must be marked with `// rustc internal` in its own group.
190 let mut next_feature_is_rustc_internal = false;
192 contents.lines().zip(1..)
193 .filter_map(|(line, line_number)| {
194 let line = line.trim();
195 if line.starts_with("// rustc internal") {
196 next_feature_is_rustc_internal = true;
198 } else if line.is_empty() {
199 next_feature_is_rustc_internal = false;
203 let mut parts = line.split(',');
204 let level = match parts.next().map(|l| l.trim().trim_left_matches('(')) {
205 Some("active") => Status::Unstable,
206 Some("removed") => Status::Removed,
207 Some("accepted") => Status::Stable,
210 let name = parts.next().unwrap().trim();
211 let since = parts.next().unwrap().trim().trim_matches('"');
212 let issue_str = parts.next().unwrap().trim();
213 let tracking_issue = if issue_str.starts_with("None") {
214 if level == Status::Unstable && !next_feature_is_rustc_internal {
218 "libsyntax/feature_gate.rs:{}: no tracking issue for feature {}",
225 next_feature_is_rustc_internal = false;
226 let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap();
227 Some(s.parse().unwrap())
229 Some((name.to_owned(),
232 since: since.to_owned(),
233 has_gate_test: false,
240 pub fn collect_lib_features(base_src_path: &Path) -> Features {
241 let mut lib_features = Features::new();
243 // This library feature is defined in the `compiler_builtins` crate, which
244 // has been moved out-of-tree. Now it can no longer be auto-discovered by
245 // `tidy`, because we need to filter out its (submodule) directory. Manually
246 // add it to the set of known library features so we can still generate docs.
247 lib_features.insert("compiler_builtins_lib".to_owned(), Feature {
248 level: Status::Unstable,
249 since: String::new(),
250 has_gate_test: false,
251 tracking_issue: None,
254 map_lib_features(base_src_path,
256 if let Ok((name, feature)) = res {
257 if lib_features.contains_key(name) {
260 lib_features.insert(name.to_owned(), feature);
266 fn get_and_check_lib_features(base_src_path: &Path,
268 lang_features: &Features) -> Features {
269 let mut lib_features = Features::new();
270 map_lib_features(base_src_path,
271 &mut |res, file, line| {
274 let mut check_features = |f: &Feature, list: &Features, display: &str| {
275 if let Some(ref s) = list.get(name) {
276 if f.tracking_issue != s.tracking_issue {
278 "{}:{}: mismatches the `issue` in {}",
285 check_features(&f, &lang_features, "corresponding lang feature");
286 check_features(&f, &lib_features, "previous");
287 lib_features.insert(name.to_owned(), f);
290 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
298 fn map_lib_features(base_src_path: &Path,
299 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) {
300 let mut contents = String::new();
301 super::walk(base_src_path,
302 &mut |path| super::filter_dirs(path) || path.ends_with("src/test"),
304 let filename = file.file_name().unwrap().to_string_lossy();
305 if !filename.ends_with(".rs") || filename == "features.rs" ||
306 filename == "diagnostic_list.rs" {
310 contents.truncate(0);
311 t!(t!(File::open(&file), &file).read_to_string(&mut contents));
313 let mut becoming_feature: Option<(String, Feature)> = None;
314 for (i, line) in contents.lines().enumerate() {
317 mf(Err($msg), file, i + 1);
321 if let Some((ref name, ref mut f)) = becoming_feature {
322 if f.tracking_issue.is_none() {
323 f.tracking_issue = find_attr_val(line, "issue")
324 .map(|s| s.parse().unwrap());
326 if line.ends_with(']') {
327 mf(Ok((name, f.clone())), file, i + 1);
328 } else if !line.ends_with(',') && !line.ends_with('\\') {
329 // We need to bail here because we might have missed the
330 // end of a stability attribute above because the ']'
331 // might not have been at the end of the line.
332 // We could then get into the very unfortunate situation that
333 // we continue parsing the file assuming the current stability
334 // attribute has not ended, and ignoring possible feature
335 // attributes in the process.
336 err!("malformed stability attribute");
341 becoming_feature = None;
342 if line.contains("rustc_const_unstable(") {
343 // const fn features are handled specially
344 let feature_name = match find_attr_val(line, "feature") {
346 None => err!("malformed stability attribute"),
348 let feature = Feature {
349 level: Status::Unstable,
350 since: "None".to_owned(),
351 has_gate_test: false,
352 // Whether there is a common tracking issue
353 // for these feature gates remains an open question
354 // https://github.com/rust-lang/rust/issues/24111#issuecomment-340283184
355 // But we take 24111 otherwise they will be shown as
356 // "internal to the compiler" which they are not.
357 tracking_issue: Some(24111),
359 mf(Ok((feature_name, feature)), file, i + 1);
362 let level = if line.contains("[unstable(") {
364 } else if line.contains("[stable(") {
369 let feature_name = match find_attr_val(line, "feature") {
371 None => err!("malformed stability attribute"),
373 let since = match find_attr_val(line, "since") {
375 None if level == Status::Stable => {
376 err!("malformed stability attribute");
380 let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap());
382 let feature = Feature {
384 since: since.to_owned(),
385 has_gate_test: false,
388 if line.contains(']') {
389 mf(Ok((feature_name, feature)), file, i + 1);
391 becoming_feature = Some((feature_name.to_owned(), feature));