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()..].split(' ').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-{feature_name}' comment"
139 ));
140 }
141 f.has_gate_test = true;
142 }
143 None => {
144 err(&format!(
145 "gate-test test found referencing a nonexistent feature '{feature_name}'"
146 ));
147 }
148 }
149 }
150 },
151 );
152
153 let gate_untested = features
156 .iter()
157 .filter(|&(_, f)| f.level == Status::Unstable)
158 .filter(|&(_, f)| !f.has_gate_test)
159 .collect::<Vec<_>>();
160
161 for &(name, _) in gate_untested.iter() {
162 println!("Expected a gate test for the feature '{name}'.");
163 println!(
164 "Hint: create a failing test file named 'tests/ui/feature-gates/feature-gate-{}.rs',\
165 \n with its failures due to missing usage of `#![feature({})]`.",
166 name.replace("_", "-"),
167 name
168 );
169 println!(
170 "Hint: If you already have such a test and don't want to rename it,\
171 \n you can also add a // gate-test-{name} line to the test file."
172 );
173 }
174
175 if !gate_untested.is_empty() {
176 tidy_error!(bad, "Found {} features without a gate test.", gate_untested.len());
177 }
178
179 let (version, channel) = get_version_and_channel(src_path);
180
181 let all_features_iter = features
182 .iter()
183 .map(|feat| (feat, "lang"))
184 .chain(lib_features.iter().map(|feat| (feat, "lib")));
185 for ((feature_name, feature), kind) in all_features_iter {
186 let since = if let Some(since) = feature.since { since } else { continue };
187 let file = feature.file.display();
188 let line = feature.line;
189 if since > version && since != Version::CurrentPlaceholder {
190 tidy_error!(
191 bad,
192 "{file}:{line}: The stabilization version {since} of {kind} feature `{feature_name}` is newer than the current {version}"
193 );
194 }
195 if channel == "nightly" && since == version {
196 tidy_error!(
197 bad,
198 "{file}:{line}: The stabilization version {since} of {kind} feature `{feature_name}` is written out but should be {}",
199 version::VERSION_PLACEHOLDER
200 );
201 }
202 if channel != "nightly" && since == Version::CurrentPlaceholder {
203 tidy_error!(
204 bad,
205 "{file}:{line}: The placeholder use of {kind} feature `{feature_name}` is not allowed on the {channel} channel",
206 );
207 }
208 }
209
210 if *bad {
211 return CollectedFeatures { lib: lib_features, lang: features };
212 }
213
214 if verbose {
215 let mut lines = Vec::new();
216 lines.extend(format_features(&features, "lang"));
217 lines.extend(format_features(&lib_features, "lib"));
218
219 lines.sort();
220 for line in lines {
221 println!("* {line}");
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, bad: &mut bool) -> Features {
277 let mut features = Features::new();
278 collect_lang_features_in(&mut features, base_compiler_path, "accepted.rs", bad);
279 collect_lang_features_in(&mut features, base_compiler_path, "removed.rs", bad);
280 collect_lang_features_in(&mut features, base_compiler_path, "unstable.rs", bad);
281 features
282}
283
284fn collect_lang_features_in(features: &mut Features, base: &Path, file: &str, bad: &mut bool) {
285 let path = base.join("rustc_feature").join("src").join(file);
286 let contents = t!(fs::read_to_string(&path));
287
288 let mut next_feature_omits_tracking_issue = false;
292
293 let mut in_feature_group = false;
294 let mut prev_names = vec![];
295
296 let lines = contents.lines().zip(1..);
297 let mut doc_comments: Vec<String> = Vec::new();
298 for (line, line_number) in lines {
299 let line = line.trim();
300
301 match line {
303 "// no-tracking-issue-start" => {
304 next_feature_omits_tracking_issue = true;
305 continue;
306 }
307 "// no-tracking-issue-end" => {
308 next_feature_omits_tracking_issue = false;
309 continue;
310 }
311 _ => {}
312 }
313
314 if line.starts_with(FEATURE_GROUP_START_PREFIX) {
315 if in_feature_group {
316 tidy_error!(
317 bad,
318 "{}:{}: \
319 new feature group is started without ending the previous one",
320 path.display(),
321 line_number,
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 tidy_error!(
355 bad,
356 "{}:{}: failed to parse since: {} ({:?})",
357 path.display(),
358 line_number,
359 since_str,
360 err,
361 );
362 None
363 }
364 };
365 if in_feature_group {
366 if prev_names.last() > Some(&name) {
367 let correct_index = match prev_names.binary_search(&name) {
370 Ok(_) => {
371 tidy_error!(
373 bad,
374 "{}:{}: duplicate feature {}",
375 path.display(),
376 line_number,
377 name,
378 );
379 continue;
381 }
382 Err(index) => index,
383 };
384
385 let correct_placement = if correct_index == 0 {
386 "at the beginning of the feature group".to_owned()
387 } else if correct_index == prev_names.len() {
388 "at the end of the feature group".to_owned()
391 } else {
392 format!(
393 "between {} and {}",
394 prev_names[correct_index - 1],
395 prev_names[correct_index],
396 )
397 };
398
399 tidy_error!(
400 bad,
401 "{}:{}: feature {} is not sorted by feature name (should be {})",
402 path.display(),
403 line_number,
404 name,
405 correct_placement,
406 );
407 }
408 prev_names.push(name);
409 }
410
411 let issue_str = parts.next().unwrap().trim();
412 let tracking_issue = if issue_str.starts_with("None") {
413 if level == Status::Unstable && !next_feature_omits_tracking_issue {
414 tidy_error!(
415 bad,
416 "{}:{}: no tracking issue for feature {}",
417 path.display(),
418 line_number,
419 name,
420 );
421 }
422 None
423 } else {
424 let s = issue_str.split('(').nth(1).unwrap().split(')').next().unwrap();
425 Some(s.parse().unwrap())
426 };
427 match features.entry(name.to_owned()) {
428 Entry::Occupied(e) => {
429 tidy_error!(
430 bad,
431 "{}:{} feature {name} already specified with status '{}'",
432 path.display(),
433 line_number,
434 e.get().level,
435 );
436 }
437 Entry::Vacant(e) => {
438 e.insert(Feature {
439 level,
440 since,
441 has_gate_test: false,
442 tracking_issue,
443 file: path.to_path_buf(),
444 line: line_number,
445 description: if doc_comments.is_empty() {
446 None
447 } else {
448 Some(doc_comments.join(" "))
449 },
450 });
451 }
452 }
453 doc_comments.clear();
454 }
455}
456
457fn get_and_check_lib_features(
458 base_src_path: &Path,
459 bad: &mut bool,
460 lang_features: &Features,
461) -> Features {
462 let mut lib_features = Features::new();
463 map_lib_features(base_src_path, &mut |res, file, line| match res {
464 Ok((name, f)) => {
465 let mut check_features = |f: &Feature, list: &Features, display: &str| {
466 if let Some(s) = list.get(name)
467 && f.tracking_issue != s.tracking_issue
468 && f.level != Status::Accepted
469 {
470 tidy_error!(
471 bad,
472 "{}:{}: feature gate {} has inconsistent `issue`: \"{}\" mismatches the {} `issue` of \"{}\"",
473 file.display(),
474 line,
475 name,
476 f.tracking_issue_display(),
477 display,
478 s.tracking_issue_display(),
479 );
480 }
481 };
482 check_features(&f, lang_features, "corresponding lang feature");
483 check_features(&f, &lib_features, "previous");
484 lib_features.insert(name.to_owned(), f);
485 }
486 Err(msg) => {
487 tidy_error!(bad, "{}:{}: {}", file.display(), line, msg);
488 }
489 });
490 lib_features
491}
492
493fn map_lib_features(
494 base_src_path: &Path,
495 mf: &mut (dyn Send + Sync + FnMut(Result<(&str, Feature), &str>, &Path, usize)),
496) {
497 walk(
498 base_src_path,
499 |path, _is_dir| filter_dirs(path) || path.ends_with("tests"),
500 &mut |entry, contents| {
501 let file = entry.path();
502 let filename = file.file_name().unwrap().to_string_lossy();
503 if !filename.ends_with(".rs")
504 || filename == "features.rs"
505 || filename == "diagnostic_list.rs"
506 || filename == "error_codes.rs"
507 {
508 return;
509 }
510
511 if !contents.contains("stable(") {
516 return;
517 }
518
519 let handle_issue_none = |s| match s {
520 "none" => None,
521 issue => {
522 let n = issue.parse().expect("issue number is not a valid integer");
523 assert_ne!(n, 0, "\"none\" should be used when there is no issue, not \"0\"");
524 NonZeroU32::new(n)
525 }
526 };
527 let mut becoming_feature: Option<(&str, Feature)> = None;
528 let mut iter_lines = contents.lines().enumerate().peekable();
529 while let Some((i, line)) = iter_lines.next() {
530 macro_rules! err {
531 ($msg:expr) => {{
532 mf(Err($msg), file, i + 1);
533 continue;
534 }};
535 }
536
537 if static_regex!(r"^\s*//").is_match(line) {
539 continue;
540 }
541
542 if let Some((name, ref mut f)) = becoming_feature {
543 if f.tracking_issue.is_none() {
544 f.tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
545 }
546 if line.ends_with(']') {
547 mf(Ok((name, f.clone())), file, i + 1);
548 } else if !line.ends_with(',') && !line.ends_with('\\') && !line.ends_with('"')
549 {
550 err!("malformed stability attribute");
558 } else {
559 continue;
560 }
561 }
562 becoming_feature = None;
563 if line.contains("rustc_const_unstable(") {
564 let feature_name = match find_attr_val(line, "feature").or_else(|| {
566 iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature"))
567 }) {
568 Some(name) => name,
569 None => err!("malformed stability attribute: missing `feature` key"),
570 };
571 let feature = Feature {
572 level: Status::Unstable,
573 since: None,
574 has_gate_test: false,
575 tracking_issue: find_attr_val(line, "issue").and_then(handle_issue_none),
576 file: file.to_path_buf(),
577 line: i + 1,
578 description: None,
579 };
580 mf(Ok((feature_name, feature)), file, i + 1);
581 continue;
582 }
583 let level = if line.contains("[unstable(") {
584 Status::Unstable
585 } else if line.contains("[stable(") {
586 Status::Accepted
587 } else {
588 continue;
589 };
590 let feature_name = match find_attr_val(line, "feature")
591 .or_else(|| iter_lines.peek().and_then(|next| find_attr_val(next.1, "feature")))
592 {
593 Some(name) => name,
594 None => err!("malformed stability attribute: missing `feature` key"),
595 };
596 let since = match find_attr_val(line, "since").map(|x| x.parse()) {
597 Some(Ok(since)) => Some(since),
598 Some(Err(_err)) => {
599 err!("malformed stability attribute: can't parse `since` key");
600 }
601 None if level == Status::Accepted => {
602 err!("malformed stability attribute: missing the `since` key");
603 }
604 None => None,
605 };
606 let tracking_issue = find_attr_val(line, "issue").and_then(handle_issue_none);
607
608 let feature = Feature {
609 level,
610 since,
611 has_gate_test: false,
612 tracking_issue,
613 file: file.to_path_buf(),
614 line: i + 1,
615 description: None,
616 };
617 if line.contains(']') {
618 mf(Ok((feature_name, feature)), file, i + 1);
619 } else {
620 becoming_feature = Some((feature_name, feature));
621 }
622 }
623 },
624 );
625}