1//! Checks that test revision names appearing in header directives and error
2//! annotations have actually been declared in `revisions`.
34// FIXME(jieyouxu) Ideally these checks would be integrated into compiletest's
5// own directive and revision handling, but for now they've been split out as a
6// separate `tidy` check to avoid making compiletest even messier.
78use std::collections::{BTreeSet, HashMap, HashSet};
9use std::path::Path;
10use std::sync::OnceLock;
1112use ignore::DirEntry;
13use regex::Regex;
1415use crate::iter_header::{HeaderLine, iter_header};
16use crate::walk::{filter_dirs, filter_not_rust, walk};
1718pub fn check(tests_path: impl AsRef<Path>, bad: &mut bool) {
19 walk(
20 tests_path.as_ref(),
21 |path, is_dir| {
22 filter_dirs(path) || filter_not_rust(path) || {
23// Auxiliary source files for incremental tests can refer to revisions
24 // declared by the main file, which this check doesn't handle.
25is_dir && path.file_name().is_some_and(|name| name == "auxiliary")
26 }
27 },
28&mut |entry, contents| visit_test_file(entry, contents, bad),
29 );
30}
3132fn visit_test_file(entry: &DirEntry, contents: &str, bad: &mut bool) {
33let mut revisions = HashSet::new();
34let mut unused_revision_names = HashSet::new();
3536// Maps each mentioned revision to the first line it was mentioned on.
37let mut mentioned_revisions = HashMap::<&str, usize>::new();
38let mut add_mentioned_revision = |line_number: usize, revision| {
39let first_line = mentioned_revisions.entry(revision).or_insert(line_number);
40*first_line = (*first_line).min(line_number);
41 };
4243// Scan all `//@` headers to find declared revisions and mentioned revisions.
44iter_header(contents, &mut |HeaderLine { line_number, revision, directive }| {
45if let Some(revs) = directive.strip_prefix("revisions:") {
46 revisions.extend(revs.split_whitespace());
47 } else if let Some(revs) = directive.strip_prefix("unused-revision-names:") {
48 unused_revision_names.extend(revs.split_whitespace());
49 }
5051if let Some(revision) = revision {
52 add_mentioned_revision(line_number, revision);
53 }
54 });
5556// If a wildcard appears in `unused-revision-names`, skip all revision name
57 // checking for this file.
58if unused_revision_names.contains(&"*") {
59return;
60 }
6162// Scan all `//[rev]~` error annotations to find mentioned revisions.
63for_each_error_annotation_revision(contents, &mut |ErrorAnnRev { line_number, revision }| {
64 add_mentioned_revision(line_number, revision);
65 });
6667let path = entry.path().display();
6869// Fail if any revision names appear in both places, since that's probably a mistake.
70for rev in revisions.intersection(&unused_revision_names).copied().collect::<BTreeSet<_>>() {
71tidy_error!(
72 bad,
73"revision name [{rev}] appears in both `revisions` and `unused-revision-names` in {path}"
74);
75 }
7677// Compute the set of revisions that were mentioned but not declared,
78 // sorted by the first line number they appear on.
79let mut bad_revisions = mentioned_revisions
80 .into_iter()
81 .filter(|(rev, _)| !revisions.contains(rev) && !unused_revision_names.contains(rev))
82 .map(|(rev, line_number)| (line_number, rev))
83 .collect::<Vec<_>>();
84 bad_revisions.sort();
8586for (line_number, rev) in bad_revisions {
87tidy_error!(bad, "unknown revision [{rev}] at {path}:{line_number}");
88 }
89}
9091struct ErrorAnnRev<'a> {
92 line_number: usize,
93 revision: &'a str,
94}
9596fn for_each_error_annotation_revision<'a>(
97 contents: &'a str,
98 callback: &mut dyn FnMut(ErrorAnnRev<'a>),
99) {
100let error_regex = {
101// Simplified from the regex used by `parse_expected` in `src/tools/compiletest/src/errors.rs`,
102 // because we only care about extracting revision names.
103static RE: OnceLock<Regex> = OnceLock::new();
104 RE.get_or_init(|| Regex::new(r"//\[(?<revs>[^]]*)\]~").unwrap())
105 };
106107for (line_number, line) in (1..).zip(contents.lines()) {
108let Some(captures) = error_regex.captures(line) else { continue };
109110for revision in captures.name("revs").unwrap().as_str().split(',') {
111 callback(ErrorAnnRev { line_number, revision });
112 }
113 }
114}