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}
58impl Feature {
59 fn tracking_issue_display(&self) -> impl fmt::Display {
60 match self.tracking_issue {
61 None => "none".to_string(),
62 Some(x) => x.to_string(),
63 }
64 }
65}
66
67pub type Features = HashMap<String, Feature>;
68
69pub struct CollectedFeatures {
70 pub lib: Features,
71 pub lang: Features,
72}
73
74pub fn collect_lib_features(base_src_path: &Path) -> Features {
76 let mut lib_features = Features::new();
77
78 map_lib_features(base_src_path, &mut |res, _, _| {
79 if let Ok((name, feature)) = res {
80 lib_features.insert(name.to_owned(), feature);
81 }
82 });
83 lib_features
84}
85
86pub fn check(
87 src_path: &Path,
88 tests_path: &Path,
89 compiler_path: &Path,
90 lib_path: &Path,
91 bad: &mut bool,
92 verbose: bool,
93) -> CollectedFeatures {
94 let mut features = collect_lang_features(compiler_path, bad);
95 assert!(!features.is_empty());
96
97 let lib_features = get_and_check_lib_features(lib_path, bad, &features);
98 assert!(!lib_features.is_empty());
99
100 walk_many(
101 &[
102 &tests_path.join("ui"),
103 &tests_path.join("ui-fulldeps"),
104 &tests_path.join("rustdoc-ui"),
105 &tests_path.join("rustdoc"),
106 ],
107 |path, _is_dir| {
108 filter_dirs(path)
109 || filter_not_rust(path)
110 || path.file_name() == Some(OsStr::new("features.rs"))
111 || path.file_name() == Some(OsStr::new("diagnostic_list.rs"))
112 },
113 &mut |entry, contents| {
114 let file = entry.path();
115 let filename = file.file_name().unwrap().to_string_lossy();
116 let filen_underscore = filename.replace('-', "_").replace(".rs", "");
117 let filename_gate = test_filen_gate(&filen_underscore, &mut features);
118
119 for (i, line) in contents.lines().enumerate() {
120 let mut err = |msg: &str| {
121 tidy_error!(bad, "{}:{}: {}", file.display(), i + 1, msg);
122 };
123
124 let gate_test_str = "gate-test-";
125
126 let feature_name = match line.find(gate_test_str) {
127 Some(i) => line[i + gate_test_str.len()..].splitn(2, ' ').next().unwrap(),
129 None => continue,
130 };
131 match features.get_mut(feature_name) {
132 Some(f) => {
133 if filename_gate == Some(feature_name) {
134 err(&format!(
135 "The file is already marked as gate test \
136 through its name, no need for a \
137 'gate-test-{}' comment",
138 feature_name
139 ));
140 }
141 f.has_gate_test = true;
142 }
143 None => {
144 err(&format!(
145 "gate-test test found referencing a nonexistent feature '{}'",
146 feature_name
147 ));
148 }
149 }
150 }
151 },
152 );
153
154 let gate_untested = features
157 .iter()
158 .filter(|&(_, f)| f.level == Status::Unstable)
159 .filter(|&(_, f)| !f.has_gate_test)
160 .collect::<Vec<_>>();
161
162 for &(name, _) in gate_untested.iter() {
163 println!("Expected a gate test for the feature '{name}'.");
164 println!(
165 "Hint: create a failing test file named 'tests/ui/feature-gates/feature-gate-{}.rs',\
166 \n with its failures due to missing usage of `#![feature({})]`.",
167 name.replace("_", "-"),
168 name
169 );
170 println!(
171 "Hint: If you already have such a test and don't want to rename it,\
172 \n you can also add a // gate-test-{} line to the test file.",
173 name
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 for (line, line_number) in lines {
300 let line = line.trim();
301
302 match line {
304 "// no-tracking-issue-start" => {
305 next_feature_omits_tracking_issue = true;
306 continue;
307 }
308 "// no-tracking-issue-end" => {
309 next_feature_omits_tracking_issue = false;
310 continue;
311 }
312 _ => {}
313 }
314
315 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
316 if in_feature_group {
317 tidy_error!(
318 bad,
319 "{}:{}: \
320 new feature group is started without ending the previous one",
321 path.display(),
322 line_number,
323 );
324 }
325
326 in_feature_group = true;
327 prev_names = vec![];
328 continue;
329 } else if line.starts_with(FEATURE_GROUP_END_PREFIX) {
330 in_feature_group = false;
331 prev_names = vec![];
332 continue;
333 }
334
335 let mut parts = line.split(',');
336 let level = match parts.next().map(|l| l.trim().trim_start_matches('(')) {
337 Some("unstable") => Status::Unstable,
338 Some("incomplete") => Status::Unstable,
339 Some("internal") => Status::Unstable,
340 Some("removed") => Status::Removed,
341 Some("accepted") => Status::Accepted,
342 _ => continue,
343 };
344 let name = parts.next().unwrap().trim();
345
346 let since_str = parts.next().unwrap().trim().trim_matches('"');
347 let since = match since_str.parse() {
348 Ok(since) => Some(since),
349 Err(err) => {
350 tidy_error!(
351 bad,
352 "{}:{}: failed to parse since: {} ({:?})",
353 path.display(),
354 line_number,
355 since_str,
356 err,
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 tidy_error!(
369 bad,
370 "{}:{}: duplicate feature {}",
371 path.display(),
372 line_number,
373 name,
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 tidy_error!(
396 bad,
397 "{}:{}: feature {} is not sorted by feature name (should be {})",
398 path.display(),
399 line_number,
400 name,
401 correct_placement,
402 );
403 }
404 prev_names.push(name);
405 }
406
407 let issue_str = parts.next().unwrap().trim();
408 let tracking_issue = if issue_str.starts_with("None") {
409 if level == Status::Unstable && !next_feature_omits_tracking_issue {
410 tidy_error!(
411 bad,
412 "{}:{}: no tracking issue for feature {}",
413 path.display(),
414 line_number,
415 name,
416 );
417 }
418 None
419 } else {
420 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
421 Some(s.parse().unwrap())
422 };
423 match features.entry(name.to_owned()) {
424 Entry::Occupied(e) => {
425 tidy_error!(
426 bad,
427 "{}:{} feature {name} already specified with status '{}'",
428 path.display(),
429 line_number,
430 e.get().level,
431 );
432 }
433 Entry::Vacant(e) => {
434 e.insert(Feature {
435 level,
436 since,
437 has_gate_test: false,
438 tracking_issue,
439 file: path.to_path_buf(),
440 line: line_number,
441 });
442 }
443 }
444 }
445}
446
447fn get_and_check_lib_features(
448 base_src_path: &Path,
449 bad: &mut bool,
450 lang_features: &Features,
451) -> Features {
452 let mut lib_features = Features::new();
453 map_lib_features(base_src_path, &mut |res, file, line| match res {
454 Ok((name, f)) => {
455 let mut check_features = |f: &Feature, list: &Features, display: &str| {
456 if let Some(ref s) = list.get(name) {
457 if f.tracking_issue != s.tracking_issue && f.level != Status::Accepted {
458 tidy_error!(
459 bad,
460 "{}:{}: feature gate {} has inconsistent `issue`: \"{}\" mismatches the {} `issue` of \"{}\"",
461 file.display(),
462 line,
463 name,
464 f.tracking_issue_display(),
465 display,
466 s.tracking_issue_display(),
467 );
468 }
469 }
470 };
471 check_features(&f, &lang_features, "corresponding lang feature");
472 check_features(&f, &lib_features, "previous");
473 lib_features.insert(name.to_owned(), f);
474 }
475 Err(msg) => {
476 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
477 }
478 });
479 lib_features
480}
481
482fn map_lib_features(
483 base_src_path: &Path,
484 mf: &mut (dyn Send + Sync + FnMut(Result<(&str, Feature), &str>, &Path, usize)),
485) {
486 walk(
487 base_src_path,
488 |path, _is_dir| filter_dirs(path) || path.ends_with("tests"),
489 &mut |entry, contents| {
490 let file = entry.path();
491 let filename = file.file_name().unwrap().to_string_lossy();
492 if !filename.ends_with(".rs")
493 || filename == "features.rs"
494 || filename == "diagnostic_list.rs"
495 || filename == "error_codes.rs"
496 {
497 return;
498 }
499
500 if !contents.contains("stable(") {
505 return;
506 }
507
508 let handle_issue_none = |s| match s {
509 "none" => None,
510 issue => {
511 let n = issue.parse().expect("issue number is not a valid integer");
512 assert_ne!(n, 0, "\"none\" should be used when there is no issue, not \"0\"");
513 NonZeroU32::new(n)
514 }
515 };
516 let mut becoming_feature: Option<(&str, Feature)> = None;
517 let mut iter_lines = contents.lines().enumerate().peekable();
518 while let Some((i, line)) = iter_lines.next() {
519 macro_rules! err {
520 ($msg:expr) => {{
521 mf(Err($msg), file, i + 1);
522 continue;
523 }};
524 }
525
526 if static_regex!(r"^\s*//").is_match(line) {
528 continue;
529 }
530
531 if let Some((ref name, ref mut f)) = becoming_feature {
532 if f.tracking_issue.is_none() {
533 f.tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
534 }
535 if line.ends_with(']') {
536 mf(Ok((name, f.clone())), file, i + 1);
537 } else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"')
538 {
539 err!("malformed stability attribute");
547 } else {
548 continue;
549 }
550 }
551 becoming_feature = None;
552 if line.contains("rustc_const_unstable(") {
553 let feature_name = match find_attr_val(line, "feature").or_else(|| {
555 iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature"))
556 }) {
557 Some(name) => name,
558 None => err!("malformed stability attribute: missing `feature` key"),
559 };
560 let feature = Feature {
561 level: Status::Unstable,
562 since: None,
563 has_gate_test: false,
564 tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
565 file: file.to_path_buf(),
566 line: i + 1,
567 };
568 mf(Ok((feature_name, feature)), file, i + 1);
569 continue;
570 }
571 let level = if line.contains("[unstable(") {
572 Status::Unstable
573 } else if line.contains("[stable(") {
574 Status::Accepted
575 } else {
576 continue;
577 };
578 let feature_name = match find_attr_val(line, "feature")
579 .or_else(|| iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature")))
580 {
581 Some(name) => name,
582 None => err!("malformed stability attribute: missing `feature` key"),
583 };
584 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
585 Some(Ok(since)) => Some(since),
586 Some(Err(_err)) => {
587 err!("malformed stability attribute: can't parse `since` key");
588 }
589 None if level == Status::Accepted => {
590 err!("malformed stability attribute: missing the `since` key");
591 }
592 None => None,
593 };
594 let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
595
596 let feature = Feature {
597 level,
598 since,
599 has_gate_test: false,
600 tracking_issue,
601 file: file.to_path_buf(),
602 line: i + 1,
603 };
604 if line.contains(']') {
605 mf(Ok((feature_name, feature)), file, i + 1);
606 } else {
607 becoming_feature = Some((feature_name, feature));
608 }
609 }
610 },
611 );
612}