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