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