1use std::collections::hash_map::{Entry, HashMap};
13use std::ffi::OsStr;
14use std::num::NonZeroU32;
15use std::path::{Path, PathBuf};
16use std::{fmt, fs};
17
18use crate::walk::{filter_dirs, filter_not_rust, walk, walk_many};
19
20#[cfg(test)]
21mod tests;
22
23mod version;
24use version::Version;
25
26const FEATURE_GROUP_START_PREFIX: &str = "// feature-group-start";
27const FEATURE_GROUP_END_PREFIX: &str = "// feature-group-end";
28
29#[derive(Debug, PartialEq, Clone)]
30#[cfg_attr(feature = "build-metrics", derive(serde::Serialize))]
31pub enum Status {
32 Accepted,
33 Removed,
34 Unstable,
35}
36
37impl fmt::Display for Status {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 let as_str = match *self {
40 Status::Accepted => "accepted",
41 Status::Unstable => "unstable",
42 Status::Removed => "removed",
43 };
44 fmt::Display::fmt(as_str, f)
45 }
46}
47
48#[derive(Debug, Clone)]
49#[cfg_attr(feature = "build-metrics", derive(serde::Serialize))]
50pub struct Feature {
51 pub level: Status,
52 pub since: Option<Version>,
53 pub has_gate_test: bool,
54 pub tracking_issue: Option<NonZeroU32>,
55 pub file: PathBuf,
56 pub line: usize,
57 pub description: Option<String>,
58}
59impl Feature {
60 fn tracking_issue_display(&self) -> impl fmt::Display {
61 match self.tracking_issue {
62 None => "none".to_string(),
63 Some(x) => x.to_string(),
64 }
65 }
66}
67
68pub type Features = HashMap<String, Feature>;
69
70pub struct CollectedFeatures {
71 pub lib: Features,
72 pub lang: Features,
73}
74
75pub fn collect_lib_features(base_src_path: &Path) -> Features {
77 let mut lib_features = Features::new();
78
79 map_lib_features(base_src_path, &mut |res, _, _| {
80 if let Ok((name, feature)) = res {
81 lib_features.insert(name.to_owned(), feature);
82 }
83 });
84 lib_features
85}
86
87pub fn check(
88 src_path: &Path,
89 tests_path: &Path,
90 compiler_path: &Path,
91 lib_path: &Path,
92 bad: &mut bool,
93 verbose: bool,
94) -> CollectedFeatures {
95 let mut features = collect_lang_features(compiler_path, bad);
96 assert!(!features.is_empty());
97
98 let lib_features = get_and_check_lib_features(lib_path, bad, &features);
99 assert!(!lib_features.is_empty());
100
101 walk_many(
102 &[
103 &tests_path.join("ui"),
104 &tests_path.join("ui-fulldeps"),
105 &tests_path.join("rustdoc-ui"),
106 &tests_path.join("rustdoc"),
107 ],
108 |path, _is_dir| {
109 filter_dirs(path)
110 || filter_not_rust(path)
111 || path.file_name() == Some(OsStr::new("features.rs"))
112 || path.file_name() == Some(OsStr::new("diagnostic_list.rs"))
113 },
114 &mut |entry, contents| {
115 let file = entry.path();
116 let filename = file.file_name().unwrap().to_string_lossy();
117 let filen_underscore = filename.replace('-', "_").replace(".rs", "");
118 let filename_gate = test_filen_gate(&filen_underscore, &mut features);
119
120 for (i, line) in contents.lines().enumerate() {
121 let mut err = |msg: &str| {
122 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
123 };
124
125 let gate_test_str = "gate-test-";
126
127 let feature_name = match line.find(gate_test_str) {
128 Some(i) => line[i + gate_test_str.len()..].splitn(2, ' ').next().unwrap(),
130 None => continue,
131 };
132 match features.get_mut(feature_name) {
133 Some(f) => {
134 if filename_gate == Some(feature_name) {
135 err(&format!(
136 "The file is already marked as gate test \
137 through its name, no need for a \
138 'gate-test-{}' comment",
139 feature_name
140 ));
141 }
142 f.has_gate_test = true;
143 }
144 None => {
145 err(&format!(
146 "gate-test test found referencing a nonexistent feature '{}'",
147 feature_name
148 ));
149 }
150 }
151 }
152 },
153 );
154
155 let gate_untested = features
158 .iter()
159 .filter(|&(_, f)| f.level == Status::Unstable)
160 .filter(|&(_, f)| !f.has_gate_test)
161 .collect::<Vec<_>>();
162
163 for &(name, _) in gate_untested.iter() {
164 println!("Expected a gate test for the feature '{name}'.");
165 println!(
166 "Hint: create a failing test file named 'tests/ui/feature-gates/feature-gate-{}.rs',\
167 \n with its failures due to missing usage of `#![feature({})]`.",
168 name.replace("_", "-"),
169 name
170 );
171 println!(
172 "Hint: If you already have such a test and don't want to rename it,\
173 \n you can also add a // gate-test-{} line to the test file.",
174 name
175 );
176 }
177
178 if !gate_untested.is_empty() {
179 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
180 }
181
182 let (version, channel) = get_version_and_channel(src_path);
183
184 let all_features_iter = features
185 .iter()
186 .map(|feat| (feat, "lang"))
187 .chain(lib_features.iter().map(|feat| (feat, "lib")));
188 for ((feature_name, feature), kind) in all_features_iter {
189 let since = if let Some(since) = feature.since { since } else { continue };
190 let file = feature.file.display();
191 let line = feature.line;
192 if since > version && since != Version::CurrentPlaceholder {
193 tidy_error!(
194 bad,
195 "{file}:{line}: The stabilization version {since} of {kind} feature `{feature_name}` is newer than the current {version}"
196 );
197 }
198 if channel == "nightly" && since == version {
199 tidy_error!(
200 bad,
201 "{file}:{line}: The stabilization version {since} of {kind} feature `{feature_name}` is written out but should be {}",
202 version::VERSION_PLACEHOLDER
203 );
204 }
205 if channel != "nightly" && since == Version::CurrentPlaceholder {
206 tidy_error!(
207 bad,
208 "{file}:{line}: The placeholder use of {kind} feature `{feature_name}` is not allowed on the {channel} channel",
209 );
210 }
211 }
212
213 if *bad {
214 return CollectedFeatures { lib: lib_features, lang: features };
215 }
216
217 if verbose {
218 let mut lines = Vec::new();
219 lines.extend(format_features(&features, "lang"));
220 lines.extend(format_features(&lib_features, "lib"));
221
222 lines.sort();
223 for line in lines {
224 println!("* {line}");
225 }
226 }
227
228 CollectedFeatures { lib: lib_features, lang: features }
229}
230
231fn get_version_and_channel(src_path: &Path) -> (Version, String) {
232 let version_str = t!(std::fs::read_to_string(src_path.join("version")));
233 let version_str = version_str.trim();
234 let version = t!(std::str::FromStr::from_str(&version_str).map_err(|e| format!("{e:?}")));
235 let channel_str = t!(std::fs::read_to_string(src_path.join("ci").join("channel")));
236 (version, channel_str.trim().to_owned())
237}
238
239fn format_features<'a>(
240 features: &'a Features,
241 family: &'a str,
242) -> impl Iterator<Item = String> + 'a {
243 features.iter().map(move |(name, feature)| {
244 format!(
245 "{:<32} {:<8} {:<12} {:<8}",
246 name,
247 family,
248 feature.level,
249 feature.since.map_or("None".to_owned(), |since| since.to_string())
250 )
251 })
252}
253
254fn find_attr_val<'a>(line: &'a str, attr: &str) -> Option<&'a str> {
255 let r = match attr {
256 "issue" => static_regex!(r#"issue\s*=\s*"([^"]*)""#),
257 "feature" => static_regex!(r#"feature\s*=\s*"([^"]*)""#),
258 "since" => static_regex!(r#"since\s*=\s*"([^"]*)""#),
259 _ => unimplemented!("{attr} not handled"),
260 };
261
262 r.captures(line).and_then(|c| c.get(1)).map(|m| m.as_str())
263}
264
265fn test_filen_gate<'f>(filen_underscore: &'f str, features: &mut Features) -> Option<&'f str> {
266 let prefix = "feature_gate_";
267 if let Some(suffix) = filen_underscore.strip_prefix(prefix) {
268 for (n, f) in features.iter_mut() {
269 if suffix == n {
271 f.has_gate_test = true;
272 return Some(suffix);
273 }
274 }
275 }
276 None
277}
278
279pub fn collect_lang_features(base_compiler_path: &Path, bad: &mut bool) -> Features {
280 let mut features = Features::new();
281 collect_lang_features_in(&mut features, base_compiler_path, "accepted.rs", bad);
282 collect_lang_features_in(&mut features, base_compiler_path, "removed.rs", bad);
283 collect_lang_features_in(&mut features, base_compiler_path, "unstable.rs", bad);
284 features
285}
286
287fn collect_lang_features_in(features: &mut Features, base: &Path, file: &str, bad: &mut bool) {
288 let path = base.join("rustc_feature").join("src").join(file);
289 let contents = t!(fs::read_to_string(&path));
290
291 let mut next_feature_omits_tracking_issue = false;
295
296 let mut in_feature_group = false;
297 let mut prev_names = vec![];
298
299 let lines = contents.lines().zip(1..);
300 let mut doc_comments: Vec<String> = Vec::new();
301 for (line, line_number) in lines {
302 let line = line.trim();
303
304 match line {
306 "// no-tracking-issue-start" => {
307 next_feature_omits_tracking_issue = true;
308 continue;
309 }
310 "// no-tracking-issue-end" => {
311 next_feature_omits_tracking_issue = false;
312 continue;
313 }
314 _ => {}
315 }
316
317 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
318 if in_feature_group {
319 tidy_error!(
320 bad,
321 "{}:{}: \
322 new feature group is started without ending the previous one",
323 path.display(),
324 line_number,
325 );
326 }
327
328 in_feature_group = true;
329 prev_names = vec![];
330 continue;
331 } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
332 in_feature_group = false;
333 prev_names = vec![];
334 continue;
335 }
336
337 if in_feature_group {
338 if let Some(doc_comment) = line.strip_prefix("///") {
339 doc_comments.push(doc_comment.trim().to_string());
340 continue;
341 }
342 }
343
344 let mut parts = line.split(',');
345 let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
346 Some("unstable") => Status::Unstable,
347 Some("incomplete") => Status::Unstable,
348 Some("internal") => Status::Unstable,
349 Some("removed") => Status::Removed,
350 Some("accepted") => Status::Accepted,
351 _ => continue,
352 };
353 let name = parts.next().unwrap().trim();
354
355 let since_str = parts.next().unwrap().trim().trim_matches('"');
356 let since = match since_str.parse() {
357 Ok(since) => Some(since),
358 Err(err) => {
359 tidy_error!(
360 bad,
361 "{}:{}: failed to parse since: {} ({:?})",
362 path.display(),
363 line_number,
364 since_str,
365 err,
366 );
367 None
368 }
369 };
370 if in_feature_group {
371 if prev_names.last() > Some(&name) {
372 let correct_index = match prev_names.binary_search(&name) {
375 Ok(_) => {
376 tidy_error!(
378 bad,
379 "{}:{}: duplicate feature {}",
380 path.display(),
381 line_number,
382 name,
383 );
384 continue;
386 }
387 Err(index) => index,
388 };
389
390 let correct_placement = if correct_index == 0 {
391 "at the beginning of the feature group".to_owned()
392 } else if correct_index == prev_names.len() {
393 "at the end of the feature group".to_owned()
396 } else {
397 format!(
398 "between {} and {}",
399 prev_names[correct_index - 1],
400 prev_names[correct_index],
401 )
402 };
403
404 tidy_error!(
405 bad,
406 "{}:{}: feature {} is not sorted by feature name (should be {})",
407 path.display(),
408 line_number,
409 name,
410 correct_placement,
411 );
412 }
413 prev_names.push(name);
414 }
415
416 let issue_str = parts.next().unwrap().trim();
417 let tracking_issue = if issue_str.starts_with("None") {
418 if level == Status::Unstable && !next_feature_omits_tracking_issue {
419 tidy_error!(
420 bad,
421 "{}:{}: no tracking issue for feature {}",
422 path.display(),
423 line_number,
424 name,
425 );
426 }
427 None
428 } else {
429 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
430 Some(s.parse().unwrap())
431 };
432 match features.entry(name.to_owned()) {
433 Entry::Occupied(e) => {
434 tidy_error!(
435 bad,
436 "{}:{} feature {name} already specified with status '{}'",
437 path.display(),
438 line_number,
439 e.get().level,
440 );
441 }
442 Entry::Vacant(e) => {
443 e.insert(Feature {
444 level,
445 since,
446 has_gate_test: false,
447 tracking_issue,
448 file: path.to_path_buf(),
449 line: line_number,
450 description: if doc_comments.is_empty() {
451 None
452 } else {
453 Some(doc_comments.join(" "))
454 },
455 });
456 }
457 }
458 doc_comments.clear();
459 }
460}
461
462fn get_and_check_lib_features(
463 base_src_path: &Path,
464 bad: &mut bool,
465 lang_features: &Features,
466) -> Features {
467 let mut lib_features = Features::new();
468 map_lib_features(base_src_path, &mut |res, file, line| match res {
469 Ok((name, f)) => {
470 let mut check_features = |f: &Feature, list: &Features, display: &str| {
471 if let Some(ref s) = list.get(name) {
472 if f.tracking_issue != s.tracking_issue && f.level != Status::Accepted {
473 tidy_error!(
474 bad,
475 "{}:{}: feature gate {} has inconsistent `issue`: \"{}\" mismatches the {} `issue` of \"{}\"",
476 file.display(),
477 line,
478 name,
479 f.tracking_issue_display(),
480 display,
481 s.tracking_issue_display(),
482 );
483 }
484 }
485 };
486 check_features(&f, &lang_features, "corresponding lang feature");
487 check_features(&f, &lib_features, "previous");
488 lib_features.insert(name.to_owned(), f);
489 }
490 Err(msg) => {
491 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
492 }
493 });
494 lib_features
495}
496
497fn map_lib_features(
498 base_src_path: &Path,
499 mf: &mut (dyn Send + Sync + FnMut(Result<(&str, Feature), &str>, &Path, usize)),
500) {
501 walk(
502 base_src_path,
503 |path, _is_dir| filter_dirs(path) || path.ends_with("tests"),
504 &mut |entry, contents| {
505 let file = entry.path();
506 let filename = file.file_name().unwrap().to_string_lossy();
507 if !filename.ends_with(".rs")
508 || filename == "features.rs"
509 || filename == "diagnostic_list.rs"
510 || filename == "error_codes.rs"
511 {
512 return;
513 }
514
515 if !contents.contains("stable(") {
520 return;
521 }
522
523 let handle_issue_none = |s| match s {
524 "none" => None,
525 issue => {
526 let n = issue.parse().expect("issue number is not a valid integer");
527 assert_ne!(n, 0, "\"none\" should be used when there is no issue, not \"0\"");
528 NonZeroU32::new(n)
529 }
530 };
531 let mut becoming_feature: Option<(&str, Feature)> = None;
532 let mut iter_lines = contents.lines().enumerate().peekable();
533 while let Some((i, line)) = iter_lines.next() {
534 macro_rules! err {
535 ($msg:expr) => {{
536 mf(Err($msg), file, i + 1);
537 continue;
538 }};
539 }
540
541 if static_regex!(r"^\s*//").is_match(line) {
543 continue;
544 }
545
546 if let Some((ref name, ref mut f)) = becoming_feature {
547 if f.tracking_issue.is_none() {
548 f.tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
549 }
550 if line.ends_with(']') {
551 mf(Ok((name, f.clone())), file, i + 1);
552 } else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"')
553 {
554 err!("malformed stability attribute");
562 } else {
563 continue;
564 }
565 }
566 becoming_feature = None;
567 if line.contains("rustc_const_unstable(") {
568 let feature_name = match find_attr_val(line, "feature").or_else(|| {
570 iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature"))
571 }) {
572 Some(name) => name,
573 None => err!("malformed stability attribute: missing `feature` key"),
574 };
575 let feature = Feature {
576 level: Status::Unstable,
577 since: None,
578 has_gate_test: false,
579 tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
580 file: file.to_path_buf(),
581 line: i + 1,
582 description: None,
583 };
584 mf(Ok((feature_name, feature)), file, i + 1);
585 continue;
586 }
587 let level = if line.contains("[unstable(") {
588 Status::Unstable
589 } else if line.contains("[stable(") {
590 Status::Accepted
591 } else {
592 continue;
593 };
594 let feature_name = match find_attr_val(line, "feature")
595 .or_else(|| iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature")))
596 {
597 Some(name) => name,
598 None => err!("malformed stability attribute: missing `feature` key"),
599 };
600 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
601 Some(Ok(since)) => Some(since),
602 Some(Err(_err)) => {
603 err!("malformed stability attribute: can't parse `since` key");
604 }
605 None if level == Status::Accepted => {
606 err!("malformed stability attribute: missing the `since` key");
607 }
608 None => None,
609 };
610 let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
611
612 let feature = Feature {
613 level,
614 since,
615 has_gate_test: false,
616 tracking_issue,
617 file: file.to_path_buf(),
618 line: i + 1,
619 description: None,
620 };
621 if line.contains(']') {
622 mf(Ok((feature_name, feature)), file, i + 1);
623 } else {
624 becoming_feature = Some((feature_name, feature));
625 }
626 }
627 },
628 );
629}