|
| 1 | +//! Checks that test revision names appearing in header directives and error |
| 2 | +//! annotations have actually been declared in `revisions`. |
| 3 | +
|
| 4 | +// 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. |
| 7 | + |
| 8 | +use std::collections::{BTreeSet, HashMap, HashSet}; |
| 9 | +use std::path::Path; |
| 10 | +use std::sync::OnceLock; |
| 11 | + |
| 12 | +use ignore::DirEntry; |
| 13 | +use regex::Regex; |
| 14 | + |
| 15 | +use crate::iter_header::{iter_header, HeaderLine}; |
| 16 | +use crate::walk::{filter_dirs, filter_not_rust, walk}; |
| 17 | + |
| 18 | +pub 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. |
| 25 | + is_dir && path.file_name().is_some_and(|name| name == "auxiliary") |
| 26 | + } |
| 27 | + }, |
| 28 | + &mut |entry, contents| visit_test_file(entry, contents, bad), |
| 29 | + ); |
| 30 | +} |
| 31 | + |
| 32 | +fn visit_test_file(entry: &DirEntry, contents: &str, bad: &mut bool) { |
| 33 | + let mut revisions = HashSet::new(); |
| 34 | + let mut unused_revision_names = HashSet::new(); |
| 35 | + |
| 36 | + // Maps each mentioned revision to the first line it was mentioned on. |
| 37 | + let mut mentioned_revisions = HashMap::<&str, usize>::new(); |
| 38 | + let mut add_mentioned_revision = |line_number: usize, revision| { |
| 39 | + let first_line = mentioned_revisions.entry(revision).or_insert(line_number); |
| 40 | + *first_line = (*first_line).min(line_number); |
| 41 | + }; |
| 42 | + |
| 43 | + // Scan all `//@` headers to find declared revisions and mentioned revisions. |
| 44 | + iter_header(contents, &mut |HeaderLine { line_number, revision, directive }| { |
| 45 | + if 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 | + } |
| 50 | + |
| 51 | + if let Some(revision) = revision { |
| 52 | + add_mentioned_revision(line_number, revision); |
| 53 | + } |
| 54 | + }); |
| 55 | + |
| 56 | + // If a wildcard appears in `unused-revision-names`, skip all revision name |
| 57 | + // checking for this file. |
| 58 | + if unused_revision_names.contains(&"*") { |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + // Scan all `//[rev]~` error annotations to find mentioned revisions. |
| 63 | + for_each_error_annotation_revision(contents, &mut |ErrorAnnRev { line_number, revision }| { |
| 64 | + add_mentioned_revision(line_number, revision); |
| 65 | + }); |
| 66 | + |
| 67 | + let path = entry.path().display(); |
| 68 | + |
| 69 | + // Fail if any revision names appear in both places, since that's probably a mistake. |
| 70 | + for rev in revisions.intersection(&unused_revision_names).copied().collect::<BTreeSet<_>>() { |
| 71 | + tidy_error!( |
| 72 | + bad, |
| 73 | + "revision name [{rev}] appears in both `revisions` and `unused-revision-names` in {path}" |
| 74 | + ); |
| 75 | + } |
| 76 | + |
| 77 | + // Compute the set of revisions that were mentioned but not declared, |
| 78 | + // sorted by the first line number they appear on. |
| 79 | + let 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(); |
| 85 | + |
| 86 | + for (line_number, rev) in bad_revisions { |
| 87 | + tidy_error!(bad, "unknown revision [{rev}] at {path}:{line_number}"); |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +struct ErrorAnnRev<'a> { |
| 92 | + line_number: usize, |
| 93 | + revision: &'a str, |
| 94 | +} |
| 95 | + |
| 96 | +fn for_each_error_annotation_revision<'a>( |
| 97 | + contents: &'a str, |
| 98 | + callback: &mut dyn FnMut(ErrorAnnRev<'a>), |
| 99 | +) { |
| 100 | + let 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. |
| 103 | + static RE: OnceLock<Regex> = OnceLock::new(); |
| 104 | + RE.get_or_init(|| Regex::new(r"//\[(?<revs>[^]]*)\]~").unwrap()) |
| 105 | + }; |
| 106 | + |
| 107 | + for (line_number, line) in (1..).zip(contents.lines()) { |
| 108 | + let Some(captures) = error_regex.captures(line) else { continue }; |
| 109 | + |
| 110 | + for revision in captures.name("revs").unwrap().as_str().split(',') { |
| 111 | + callback(ErrorAnnRev { line_number, revision }); |
| 112 | + } |
| 113 | + } |
| 114 | +} |
0 commit comments