]> git.proxmox.com Git - rustc.git/blame - src/tools/tidy/src/features.rs
New upstream version 1.37.0+dfsg1
[rustc.git] / src / tools / tidy / src / features.rs
CommitLineData
9fa01778 1//! Tidy check to ensure that unstable features are all in order.
a7813a04
XL
2//!
3//! This check will ensure properties like:
4//!
9fa01778
XL
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.
48663c56 10//! * Language features in a group are sorted by `since` value.
a7813a04
XL
11
12use std::collections::HashMap;
c30ab7b3 13use std::fmt;
dc9dc135 14use std::fs;
a7813a04
XL
15use std::path::Path;
16
dc9dc135 17use regex::Regex;
48663c56
XL
18
19mod version;
20use version::Version;
21
22const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
23const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
24
041b39d2 25#[derive(Debug, PartialEq, Clone)]
cc61c64b 26pub enum Status {
c30ab7b3 27 Stable,
32a655c1 28 Removed,
c30ab7b3
SL
29 Unstable,
30}
31
32impl fmt::Display for Status {
532ac7d7 33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
c30ab7b3
SL
34 let as_str = match *self {
35 Status::Stable => "stable",
36 Status::Unstable => "unstable",
32a655c1 37 Status::Removed => "removed",
c30ab7b3
SL
38 };
39 fmt::Display::fmt(as_str, f)
40 }
41}
42
041b39d2 43#[derive(Debug, Clone)]
cc61c64b
XL
44pub struct Feature {
45 pub level: Status,
48663c56 46 pub since: Option<Version>,
cc61c64b 47 pub has_gate_test: bool,
041b39d2 48 pub tracking_issue: Option<u32>,
a7813a04
XL
49}
50
041b39d2
XL
51pub type Features = HashMap<String, Feature>;
52
dc9dc135
XL
53pub struct CollectedFeatures {
54 pub lib: Features,
55 pub lang: Features,
56}
57
58// Currently only used for unstable book generation
59pub fn collect_lib_features(base_src_path: &Path) -> Features {
60 let mut lib_features = Features::new();
61
62 // This library feature is defined in the `compiler_builtins` crate, which
63 // has been moved out-of-tree. Now it can no longer be auto-discovered by
64 // `tidy`, because we need to filter out its (submodule) directory. Manually
65 // add it to the set of known library features so we can still generate docs.
66 lib_features.insert("compiler_builtins_lib".to_owned(), Feature {
67 level: Status::Unstable,
68 since: None,
69 has_gate_test: false,
70 tracking_issue: None,
71 });
72
73 map_lib_features(base_src_path,
74 &mut |res, _, _| {
75 if let Ok((name, feature)) = res {
76 lib_features.insert(name.to_owned(), feature);
77 }
78 });
79 lib_features
80}
81
82pub fn check(path: &Path, bad: &mut bool, verbose: bool) -> CollectedFeatures {
94b46f34 83 let mut features = collect_lang_features(path, bad);
c30ab7b3 84 assert!(!features.is_empty());
a7813a04 85
041b39d2 86 let lib_features = get_and_check_lib_features(path, bad, &features);
cc61c64b 87 assert!(!lib_features.is_empty());
a7813a04 88
0731742a
XL
89 super::walk_many(&[&path.join("test/ui"),
90 &path.join("test/ui-fulldeps"),
91 &path.join("test/compile-fail")],
32a655c1 92 &mut |path| super::filter_dirs(path),
dc9dc135
XL
93 &mut |entry, contents| {
94 let file = entry.path();
32a655c1
SL
95 let filename = file.file_name().unwrap().to_string_lossy();
96 if !filename.ends_with(".rs") || filename == "features.rs" ||
97 filename == "diagnostic_list.rs" {
98 return;
99 }
100
b7449926 101 let filen_underscore = filename.replace('-',"_").replace(".rs","");
7cac9316 102 let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
32a655c1 103
32a655c1
SL
104 for (i, line) in contents.lines().enumerate() {
105 let mut err = |msg: &str| {
cc61c64b 106 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
32a655c1
SL
107 };
108
109 let gate_test_str = "gate-test-";
110
32a655c1
SL
111 let feature_name = match line.find(gate_test_str) {
112 Some(i) => {
b7449926 113 line[i+gate_test_str.len()..].splitn(2, ' ').next().unwrap()
32a655c1
SL
114 },
115 None => continue,
116 };
7cac9316
XL
117 match features.get_mut(feature_name) {
118 Some(f) => {
119 if filename_is_gate_test {
120 err(&format!("The file is already marked as gate test \
121 through its name, no need for a \
122 'gate-test-{}' comment",
123 feature_name));
124 }
125 f.has_gate_test = true;
126 }
127 None => {
128 err(&format!("gate-test test found referencing a nonexistent feature '{}'",
129 feature_name));
130 }
32a655c1
SL
131 }
132 }
133 });
134
32a655c1
SL
135 // Only check the number of lang features.
136 // Obligatory testing for library features is dumb.
137 let gate_untested = features.iter()
138 .filter(|&(_, f)| f.level == Status::Unstable)
139 .filter(|&(_, f)| !f.has_gate_test)
32a655c1
SL
140 .collect::<Vec<_>>();
141
142 for &(name, _) in gate_untested.iter() {
143 println!("Expected a gate test for the feature '{}'.", name);
ff7c6d11
XL
144 println!("Hint: create a failing test file named 'feature-gate-{}.rs'\
145 \n in the 'ui' test suite, with its failures due to\
146 \n missing usage of #![feature({})].", name, name);
32a655c1
SL
147 println!("Hint: If you already have such a test and don't want to rename it,\
148 \n you can also add a // gate-test-{} line to the test file.",
149 name);
150 }
151
b7449926 152 if !gate_untested.is_empty() {
cc61c64b 153 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
32a655c1
SL
154 }
155
a7813a04 156 if *bad {
dc9dc135 157 return CollectedFeatures { lib: lib_features, lang: features };
7cac9316 158 }
a7813a04 159
dc9dc135
XL
160 if verbose {
161 let mut lines = Vec::new();
162 lines.extend(format_features(&features, "lang"));
163 lines.extend(format_features(&lib_features, "lib"));
a7813a04 164
dc9dc135
XL
165 lines.sort();
166 for line in lines {
167 println!("* {}", line);
168 }
169 } else {
170 println!("* {} features", features.len());
a7813a04 171 }
dc9dc135
XL
172
173 CollectedFeatures { lib: lib_features, lang: features }
a7813a04
XL
174}
175
48663c56
XL
176fn format_features<'a>(features: &'a Features, family: &'a str) -> impl Iterator<Item = String> + 'a {
177 features.iter().map(move |(name, feature)| {
178 format!("{:<32} {:<8} {:<12} {:<8}",
179 name,
180 family,
181 feature.level,
182 feature.since.map_or("None".to_owned(),
183 |since| since.to_string()))
184 })
185}
186
a7813a04 187fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
dc9dc135
XL
188 lazy_static::lazy_static! {
189 static ref ISSUE: Regex = Regex::new(r#"issue\s*=\s*"([^"]*)""#).unwrap();
190 static ref FEATURE: Regex = Regex::new(r#"feature\s*=\s*"([^"]*)""#).unwrap();
191 static ref SINCE: Regex = Regex::new(r#"since\s*=\s*"([^"]*)""#).unwrap();
192 }
193
194 let r = match attr {
195 "issue" => &*ISSUE,
196 "feature" => &*FEATURE,
197 "since" => &*SINCE,
198 _ => unimplemented!("{} not handled", attr),
199 };
200
48663c56
XL
201 r.captures(line)
202 .and_then(|c| c.get(1))
203 .map(|m| m.as_str())
204}
205
206#[test]
207fn test_find_attr_val() {
208 let s = r#"#[unstable(feature = "checked_duration_since", issue = "58402")]"#;
209 assert_eq!(find_attr_val(s, "feature"), Some("checked_duration_since"));
210 assert_eq!(find_attr_val(s, "issue"), Some("58402"));
211 assert_eq!(find_attr_val(s, "since"), None);
a7813a04
XL
212}
213
041b39d2 214fn test_filen_gate(filen_underscore: &str, features: &mut Features) -> bool {
dc9dc135
XL
215 let prefix = "feature_gate_";
216 if filen_underscore.starts_with(prefix) {
32a655c1 217 for (n, f) in features.iter_mut() {
dc9dc135
XL
218 // Equivalent to filen_underscore == format!("feature_gate_{}", n)
219 if &filen_underscore[prefix.len()..] == n {
32a655c1
SL
220 f.has_gate_test = true;
221 return true;
222 }
223 }
224 }
225 return false;
226}
227
94b46f34 228pub fn collect_lang_features(base_src_path: &Path, bad: &mut bool) -> Features {
0731742a 229 let contents = t!(fs::read_to_string(base_src_path.join("libsyntax/feature_gate.rs")));
a7813a04 230
9fa01778 231 // We allow rustc-internal features to omit a tracking issue.
48663c56
XL
232 // To make tidy accept omitting a tracking issue, group the list of features
233 // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
234 let mut next_feature_omits_tracking_issue = false;
235
236 let mut in_feature_group = false;
237 let mut prev_since = None;
94b46f34
XL
238
239 contents.lines().zip(1..)
240 .filter_map(|(line, line_number)| {
241 let line = line.trim();
48663c56
XL
242
243 // Within -start and -end, the tracking issue can be omitted.
244 match line {
245 "// no-tracking-issue-start" => {
246 next_feature_omits_tracking_issue = true;
247 return None;
248 }
249 "// no-tracking-issue-end" => {
250 next_feature_omits_tracking_issue = false;
251 return None;
252 }
253 _ => {}
254 }
255
256 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
257 if in_feature_group {
258 tidy_error!(
259 bad,
260 // ignore-tidy-linelength
261 "libsyntax/feature_gate.rs:{}: new feature group is started without ending the previous one",
262 line_number,
263 );
264 }
265
266 in_feature_group = true;
267 prev_since = None;
94b46f34 268 return None;
48663c56
XL
269 } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
270 in_feature_group = false;
271 prev_since = None;
94b46f34
XL
272 return None;
273 }
274
275 let mut parts = line.split(',');
9fa01778 276 let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
c30ab7b3 277 Some("active") => Status::Unstable,
32a655c1 278 Some("removed") => Status::Removed,
c30ab7b3
SL
279 Some("accepted") => Status::Stable,
280 _ => return None,
281 };
282 let name = parts.next().unwrap().trim();
48663c56
XL
283
284 let since_str = parts.next().unwrap().trim().trim_matches('"');
285 let since = match since_str.parse() {
286 Ok(since) => Some(since),
287 Err(err) => {
288 tidy_error!(
289 bad,
290 "libsyntax/feature_gate.rs:{}: failed to parse since: {} ({:?})",
291 line_number,
292 since_str,
293 err,
294 );
295 None
296 }
297 };
298 if in_feature_group {
299 if prev_since > since {
300 tidy_error!(
301 bad,
302 "libsyntax/feature_gate.rs:{}: feature {} is not sorted by since",
303 line_number,
304 name,
305 );
306 }
307 prev_since = since;
308 }
309
041b39d2
XL
310 let issue_str = parts.next().unwrap().trim();
311 let tracking_issue = if issue_str.starts_with("None") {
48663c56 312 if level == Status::Unstable && !next_feature_omits_tracking_issue {
94b46f34
XL
313 *bad = true;
314 tidy_error!(
315 bad,
316 "libsyntax/feature_gate.rs:{}: no tracking issue for feature {}",
317 line_number,
318 name,
319 );
320 }
041b39d2
XL
321 None
322 } else {
8faf50e0 323 let s = issue_str.split('(').nth(1).unwrap().split(')').nth(0).unwrap();
041b39d2
XL
324 Some(s.parse().unwrap())
325 };
32a655c1
SL
326 Some((name.to_owned(),
327 Feature {
041b39d2 328 level,
48663c56 329 since,
32a655c1 330 has_gate_test: false,
041b39d2 331 tracking_issue,
32a655c1 332 }))
c30ab7b3
SL
333 })
334 .collect()
a7813a04 335}
cc61c64b 336
041b39d2
XL
337fn get_and_check_lib_features(base_src_path: &Path,
338 bad: &mut bool,
339 lang_features: &Features) -> Features {
340 let mut lib_features = Features::new();
341 map_lib_features(base_src_path,
342 &mut |res, file, line| {
343 match res {
344 Ok((name, f)) => {
3b2f2976
XL
345 let mut check_features = |f: &Feature, list: &Features, display: &str| {
346 if let Some(ref s) = list.get(name) {
b7449926 347 if f.tracking_issue != s.tracking_issue {
3b2f2976 348 tidy_error!(bad,
b7449926 349 "{}:{}: mismatches the `issue` in {}",
3b2f2976
XL
350 file.display(),
351 line,
b7449926 352 display);
3b2f2976 353 }
041b39d2 354 }
3b2f2976
XL
355 };
356 check_features(&f, &lang_features, "corresponding lang feature");
357 check_features(&f, &lib_features, "previous");
041b39d2
XL
358 lib_features.insert(name.to_owned(), f);
359 },
360 Err(msg) => {
361 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
362 },
363 }
364
365 });
366 lib_features
367}
368
369fn map_lib_features(base_src_path: &Path,
8faf50e0 370 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize)) {
cc61c64b
XL
371 super::walk(base_src_path,
372 &mut |path| super::filter_dirs(path) || path.ends_with("src/test"),
dc9dc135
XL
373 &mut |entry, contents| {
374 let file = entry.path();
cc61c64b
XL
375 let filename = file.file_name().unwrap().to_string_lossy();
376 if !filename.ends_with(".rs") || filename == "features.rs" ||
377 filename == "diagnostic_list.rs" {
378 return;
379 }
380
dc9dc135
XL
381 // This is an early exit -- all the attributes we're concerned with must contain this:
382 // * rustc_const_unstable(
383 // * unstable(
384 // * stable(
385 if !contents.contains("stable(") {
386 return;
387 }
cc61c64b 388
dc9dc135 389 let mut becoming_feature: Option<(&str, Feature)> = None;
cc61c64b 390 for (i, line) in contents.lines().enumerate() {
041b39d2
XL
391 macro_rules! err {
392 ($msg:expr) => {{
393 mf(Err($msg), file, i + 1);
394 continue;
395 }};
cc61c64b 396 };
041b39d2
XL
397 if let Some((ref name, ref mut f)) = becoming_feature {
398 if f.tracking_issue.is_none() {
399 f.tracking_issue = find_attr_val(line, "issue")
400 .map(|s| s.parse().unwrap());
401 }
b7449926 402 if line.ends_with(']') {
041b39d2 403 mf(Ok((name, f.clone())), file, i + 1);
b7449926 404 } else if !line.ends_with(',') && !line.ends_with('\\') {
041b39d2 405 // We need to bail here because we might have missed the
b7449926 406 // end of a stability attribute above because the ']'
041b39d2
XL
407 // might not have been at the end of the line.
408 // We could then get into the very unfortunate situation that
409 // we continue parsing the file assuming the current stability
410 // attribute has not ended, and ignoring possible feature
411 // attributes in the process.
412 err!("malformed stability attribute");
413 } else {
414 continue;
415 }
416 }
417 becoming_feature = None;
abe05a73 418 if line.contains("rustc_const_unstable(") {
9fa01778 419 // `const fn` features are handled specially.
abe05a73
XL
420 let feature_name = match find_attr_val(line, "feature") {
421 Some(name) => name,
48663c56 422 None => err!("malformed stability attribute: missing `feature` key"),
abe05a73
XL
423 };
424 let feature = Feature {
425 level: Status::Unstable,
48663c56 426 since: None,
abe05a73 427 has_gate_test: false,
0731742a 428 // FIXME(#57563): #57563 is now used as a common tracking issue,
9fa01778
XL
429 // although we would like to have specific tracking issues for each
430 // `rustc_const_unstable` in the future.
0731742a 431 tracking_issue: Some(57563),
abe05a73
XL
432 };
433 mf(Ok((feature_name, feature)), file, i + 1);
434 continue;
435 }
cc61c64b
XL
436 let level = if line.contains("[unstable(") {
437 Status::Unstable
438 } else if line.contains("[stable(") {
439 Status::Stable
440 } else {
441 continue;
442 };
443 let feature_name = match find_attr_val(line, "feature") {
444 Some(name) => name,
48663c56 445 None => err!("malformed stability attribute: missing `feature` key"),
cc61c64b 446 };
48663c56
XL
447 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
448 Some(Ok(since)) => Some(since),
449 Some(Err(_err)) => {
450 err!("malformed stability attribute: can't parse `since` key");
451 },
cc61c64b 452 None if level == Status::Stable => {
48663c56 453 err!("malformed stability attribute: missing the `since` key");
cc61c64b 454 }
48663c56 455 None => None,
cc61c64b 456 };
041b39d2 457 let tracking_issue = find_attr_val(line, "issue").map(|s| s.parse().unwrap());
cc61c64b 458
041b39d2
XL
459 let feature = Feature {
460 level,
48663c56 461 since,
041b39d2
XL
462 has_gate_test: false,
463 tracking_issue,
464 };
b7449926 465 if line.contains(']') {
041b39d2
XL
466 mf(Ok((feature_name, feature)), file, i + 1);
467 } else {
dc9dc135 468 becoming_feature = Some((feature_name, feature));
cc61c64b 469 }
cc61c64b
XL
470 }
471 });
7cac9316 472}