]> git.proxmox.com Git - rustc.git/blob - src/tools/tidy/src/features.rs
New upstream version 1.65.0+dfsg1
[rustc.git] / src / tools / tidy / src / features.rs
1 //! Tidy check to ensure that unstable features are all in order.
2 //!
3 //! This check will ensure properties like:
4 //!
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.
11
12 use std::collections::HashMap;
13 use std::fmt;
14 use std::fs;
15 use std::num::NonZeroU32;
16 use std::path::Path;
17
18 use regex::Regex;
19
20 #[cfg(test)]
21 mod tests;
22
23 mod version;
24 use version::Version;
25
26 const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
27 const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
28
29 #[derive(Debug, PartialEq, Clone)]
30 pub enum Status {
31 Stable,
32 Removed,
33 Unstable,
34 }
35
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",
42 };
43 fmt::Display::fmt(as_str, f)
44 }
45 }
46
47 #[derive(Debug, Clone)]
48 pub struct Feature {
49 pub level: Status,
50 pub since: Option<Version>,
51 pub has_gate_test: bool,
52 pub tracking_issue: Option<NonZeroU32>,
53 }
54 impl Feature {
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(),
59 }
60 }
61 }
62
63 pub type Features = HashMap<String, Feature>;
64
65 pub struct CollectedFeatures {
66 pub lib: Features,
67 pub lang: Features,
68 }
69
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();
73
74 map_lib_features(base_src_path, &mut |res, _, _| {
75 if let Ok((name, feature)) = res {
76 lib_features.insert(name.to_owned(), feature);
77 }
78 });
79 lib_features
80 }
81
82 pub fn check(
83 src_path: &Path,
84 compiler_path: &Path,
85 lib_path: &Path,
86 bad: &mut bool,
87 verbose: bool,
88 ) -> CollectedFeatures {
89 let mut features = collect_lang_features(compiler_path, bad);
90 assert!(!features.is_empty());
91
92 let lib_features = get_and_check_lib_features(lib_path, bad, &features);
93 assert!(!lib_features.is_empty());
94
95 super::walk_many(
96 &[
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"),
101 ],
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"
109 {
110 return;
111 }
112
113 let filen_underscore = filename.replace('-', "_").replace(".rs", "");
114 let filename_is_gate_test = test_filen_gate(&filen_underscore, &mut features);
115
116 for (i, line) in contents.lines().enumerate() {
117 let mut err = |msg: &str| {
118 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
119 };
120
121 let gate_test_str = "gate-test-";
122
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(),
126 None => continue,
127 };
128 match features.get_mut(feature_name) {
129 Some(f) => {
130 if filename_is_gate_test {
131 err(&format!(
132 "The file is already marked as gate test \
133 through its name, no need for a \
134 'gate-test-{}' comment",
135 feature_name
136 ));
137 }
138 f.has_gate_test = true;
139 }
140 None => {
141 err(&format!(
142 "gate-test test found referencing a nonexistent feature '{}'",
143 feature_name
144 ));
145 }
146 }
147 }
148 },
149 );
150
151 // Only check the number of lang features.
152 // Obligatory testing for library features is dumb.
153 let gate_untested = features
154 .iter()
155 .filter(|&(_, f)| f.level == Status::Unstable)
156 .filter(|&(_, f)| !f.has_gate_test)
157 .collect::<Vec<_>>();
158
159 for &(name, _) in gate_untested.iter() {
160 println!("Expected a gate test for the feature '{name}'.");
161 println!(
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({})]`.",
165 name, name
166 );
167 println!(
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.",
170 name
171 );
172 }
173
174 if !gate_untested.is_empty() {
175 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
176 }
177
178 let (version, channel) = get_version_and_channel(src_path);
179
180 let all_features_iter = features
181 .iter()
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 {
187 tidy_error!(
188 bad,
189 "The stabilization version {since} of {kind} feature `{feature_name}` is newer than the current {version}"
190 );
191 }
192 if channel == "nightly" && since == version {
193 tidy_error!(
194 bad,
195 "The stabilization version {since} of {kind} feature `{feature_name}` is written out but should be {}",
196 version::VERSION_PLACEHOLDER
197 );
198 }
199 if channel != "nightly" && since == Version::CurrentPlaceholder {
200 tidy_error!(
201 bad,
202 "The placeholder use of {kind} feature `{feature_name}` is not allowed on the {channel} channel",
203 );
204 }
205 }
206
207 if *bad {
208 return CollectedFeatures { lib: lib_features, lang: features };
209 }
210
211 if verbose {
212 let mut lines = Vec::new();
213 lines.extend(format_features(&features, "lang"));
214 lines.extend(format_features(&lib_features, "lib"));
215
216 lines.sort();
217 for line in lines {
218 println!("* {line}");
219 }
220 } else {
221 println!("* {} features", features.len());
222 }
223
224 CollectedFeatures { lib: lib_features, lang: features }
225 }
226
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())
233 }
234
235 fn format_features<'a>(
236 features: &'a Features,
237 family: &'a str,
238 ) -> impl Iterator<Item = String> + 'a {
239 features.iter().map(move |(name, feature)| {
240 format!(
241 "{:<32} {:<8} {:<12} {:<8}",
242 name,
243 family,
244 feature.level,
245 feature.since.map_or("None".to_owned(), |since| since.to_string())
246 )
247 })
248 }
249
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();
255 }
256
257 let r = match attr {
258 "issue" => &*ISSUE,
259 "feature" => &*FEATURE,
260 "since" => &*SINCE,
261 _ => unimplemented!("{attr} not handled"),
262 };
263
264 r.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
265 }
266
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;
274 return true;
275 }
276 }
277 }
278 false
279 }
280
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));
285 all
286 }
287
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));
291
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;
296
297 let mut in_feature_group = false;
298 let mut prev_names = vec![];
299
300 contents
301 .lines()
302 .zip(1..)
303 .filter_map(|(line, line_number)| {
304 let line = line.trim();
305
306 // Within -start and -end, the tracking issue can be omitted.
307 match line {
308 "// no-tracking-issue-start" => {
309 next_feature_omits_tracking_issue = true;
310 return None;
311 }
312 "// no-tracking-issue-end" => {
313 next_feature_omits_tracking_issue = false;
314 return None;
315 }
316 _ => {}
317 }
318
319 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
320 if in_feature_group {
321 tidy_error!(
322 bad,
323 "{}:{}: \
324 new feature group is started without ending the previous one",
325 path.display(),
326 line_number,
327 );
328 }
329
330 in_feature_group = true;
331 prev_names = vec![];
332 return None;
333 } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
334 in_feature_group = false;
335 prev_names = vec![];
336 return None;
337 }
338
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,
345 _ => return None,
346 };
347 let name = parts.next().unwrap().trim();
348
349 let since_str = parts.next().unwrap().trim().trim_matches('"');
350 let since = match since_str.parse() {
351 Ok(since) => Some(since),
352 Err(err) => {
353 tidy_error!(
354 bad,
355 "{}:{}: failed to parse since: {} ({:?})",
356 path.display(),
357 line_number,
358 since_str,
359 err,
360 );
361 None
362 }
363 };
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) {
369 Ok(_) => {
370 // This only occurs when the feature name has already been declared.
371 tidy_error!(
372 bad,
373 "{}:{}: duplicate feature {}",
374 path.display(),
375 line_number,
376 name,
377 );
378 // skip any additional checks for this line
379 return None;
380 }
381 Err(index) => index,
382 };
383
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()
390 } else {
391 format!(
392 "between {} and {}",
393 prev_names[correct_index - 1],
394 prev_names[correct_index],
395 )
396 };
397
398 tidy_error!(
399 bad,
400 "{}:{}: feature {} is not sorted by feature name (should be {})",
401 path.display(),
402 line_number,
403 name,
404 correct_placement,
405 );
406 }
407 prev_names.push(name);
408 }
409
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 {
413 tidy_error!(
414 bad,
415 "{}:{}: no tracking issue for feature {}",
416 path.display(),
417 line_number,
418 name,
419 );
420 }
421 None
422 } else {
423 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
424 Some(s.parse().unwrap())
425 };
426 Some((name.to_owned(), Feature { level, since, has_gate_test: false, tracking_issue }))
427 })
428 .collect()
429 }
430
431 fn get_and_check_lib_features(
432 base_src_path: &Path,
433 bad: &mut bool,
434 lang_features: &Features,
435 ) -> Features {
436 let mut lib_features = Features::new();
437 map_lib_features(base_src_path, &mut |res, file, line| match res {
438 Ok((name, f)) => {
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 {
442 tidy_error!(
443 bad,
444 "{}:{}: `issue` \"{}\" mismatches the {} `issue` of \"{}\"",
445 file.display(),
446 line,
447 f.tracking_issue_display(),
448 display,
449 s.tracking_issue_display(),
450 );
451 }
452 }
453 };
454 check_features(&f, &lang_features, "corresponding lang feature");
455 check_features(&f, &lib_features, "previous");
456 lib_features.insert(name.to_owned(), f);
457 }
458 Err(msg) => {
459 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
460 }
461 });
462 lib_features
463 }
464
465 fn map_lib_features(
466 base_src_path: &Path,
467 mf: &mut dyn FnMut(Result<(&str, Feature), &str>, &Path, usize),
468 ) {
469 super::walk(
470 base_src_path,
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"
479 {
480 return;
481 }
482
483 // This is an early exit -- all the attributes we're concerned with must contain this:
484 // * rustc_const_unstable(
485 // * unstable(
486 // * stable(
487 if !contents.contains("stable(") {
488 return;
489 }
490
491 let handle_issue_none = |s| match s {
492 "none" => None,
493 issue => {
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\"");
496 NonZeroU32::new(n)
497 }
498 };
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() {
502 macro_rules! err {
503 ($msg:expr) => {{
504 mf(Err($msg), file, i + 1);
505 continue;
506 }};
507 }
508
509 lazy_static::lazy_static! {
510 static ref COMMENT_LINE: Regex = Regex::new(r"^\s*//").unwrap();
511 }
512 // exclude commented out lines
513 if COMMENT_LINE.is_match(line) {
514 continue;
515 }
516
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);
520 }
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('"')
524 {
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");
533 } else {
534 continue;
535 }
536 }
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") {
541 Some(name) => name,
542 None => err!("malformed stability attribute: missing `feature` key"),
543 };
544 let feature = Feature {
545 level: Status::Unstable,
546 since: None,
547 has_gate_test: false,
548 tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
549 };
550 mf(Ok((feature_name, feature)), file, i + 1);
551 continue;
552 }
553 let level = if line.contains("[unstable(") {
554 Status::Unstable
555 } else if line.contains("[stable(") {
556 Status::Stable
557 } else {
558 continue;
559 };
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")))
562 {
563 Some(name) => name,
564 None => err!("malformed stability attribute: missing `feature` key"),
565 };
566 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
567 Some(Ok(since)) => Some(since),
568 Some(Err(_err)) => {
569 err!("malformed stability attribute: can't parse `since` key");
570 }
571 None if level == Status::Stable => {
572 err!("malformed stability attribute: missing the `since` key");
573 }
574 None => None,
575 };
576 let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
577
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);
581 } else {
582 becoming_feature = Some((feature_name, feature));
583 }
584 }
585 },
586 );
587 }