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