tidy/
features.rs

1//! Tidy check to ensure that unstable features are all in order.
2//!
3//! This check will ensure properties like:
4//!
5//! * All stability attributes look reasonably well formed.
6//! * The set of library features is disjoint from the set of language features.
7//! * Library features have at most one stability level.
8//! * Library features have at most one `since` value.
9//! * All unstable lang features have tests to ensure they are actually unstable.
10//! * Language features in a group are sorted by feature name.
11
12use 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
74// Currently only used for unstable book generation
75pub 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                    // NB: the `splitn` always succeeds, even if the delimiter is not present.
128                    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    // Only check the number of lang features.
155    // Obligatory testing for library features is dumb.
156    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            // Equivalent to filen_underscore == format!("feature_gate_{n}")
269            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    // We allow rustc-internal features to omit a tracking issue.
291    // To make tidy accept omitting a tracking issue, group the list of features
292    // without one inside `// no-tracking-issue` and `// no-tracking-issue-end`.
293    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        // Within -start and -end, the tracking issue can be omitted.
303        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                // This assumes the user adds the feature name at the end of the list, as we're
364                // not looking ahead.
365                let correct_index = match prev_names.binary_search(&name) {
366                    Ok(_) => {
367                        // This only occurs when the feature name has already been declared.
368                        tidy_error!(
369                            bad,
370                            "{}:{}: duplicate feature {}",
371                            path.display(),
372                            line_number,
373                            name,
374                        );
375                        // skip any additional checks for this line
376                        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                    // I don't believe this is reachable given the above assumption, but it
385                    // doesn't hurt to be safe.
386                    "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            // This is an early exit -- all the attributes we're concerned with must contain this:
501            // * rustc_const_unstable(
502            // * unstable(
503            // * stable(
504            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                // exclude commented out lines
527                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                        // We need to bail here because we might have missed the
540                        // end of a stability attribute above because the ']'
541                        // might not have been at the end of the line.
542                        // We could then get into the very unfortunate situation that
543                        // we continue parsing the file assuming the current stability
544                        // attribute has not ended, and ignoring possible feature
545                        // attributes in the process.
546                        err!("malformed stability attribute");
547                    } else {
548                        continue;
549                    }
550                }
551                becoming_feature = None;
552                if line.contains("rustc_const_unstable(") {
553                    // `const fn` features are handled specially.
554                    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}