1use std::collections::{BTreeSet, HashMap, HashSet};
9use std::path::Path;
10use std::sync::OnceLock;
11
12use ignore::DirEntry;
13use regex::Regex;
14
15use crate::diagnostics::{CheckId, DiagCtx, RunningCheck};
16use crate::iter_header::{HeaderLine, iter_header};
17use crate::walk::{filter_dirs, filter_not_rust, walk};
18
19pub fn check(tests_path: &Path, diag_ctx: DiagCtx) {
20 let mut check = diag_ctx.start_check(CheckId::new("unknown_revision").path(tests_path));
21 walk(
22 tests_path,
23 |path, is_dir| {
24 filter_dirs(path) || filter_not_rust(path) || {
25 is_dir && path.file_name().is_some_and(|name| name == "auxiliary")
28 }
29 },
30 &mut |entry, contents| visit_test_file(entry, contents, &mut check),
31 );
32}
33
34fn visit_test_file(entry: &DirEntry, contents: &str, check: &mut RunningCheck) {
35 let mut revisions = HashSet::new();
36 let mut unused_revision_names = HashSet::new();
37
38 let mut mentioned_revisions = HashMap::<&str, usize>::new();
40 let mut add_mentioned_revision = |line_number: usize, revision| {
41 let first_line = mentioned_revisions.entry(revision).or_insert(line_number);
42 *first_line = (*first_line).min(line_number);
43 };
44
45 iter_header(contents, &mut |HeaderLine { line_number, revision, directive }| {
47 if let Some(revs) = directive.strip_prefix("revisions:") {
48 revisions.extend(revs.split_whitespace());
49 } else if let Some(revs) = directive.strip_prefix("unused-revision-names:") {
50 unused_revision_names.extend(revs.split_whitespace());
51 }
52
53 if let Some(revision) = revision {
54 add_mentioned_revision(line_number, revision);
55 }
56 });
57
58 if unused_revision_names.contains(&"*") {
61 return;
62 }
63
64 for_each_error_annotation_revision(contents, &mut |ErrorAnnRev { line_number, revision }| {
66 add_mentioned_revision(line_number, revision);
67 });
68
69 let path = entry.path().display();
70
71 for rev in revisions.intersection(&unused_revision_names).copied().collect::<BTreeSet<_>>() {
73 check.error(format!(
74 "revision name [{rev}] appears in both `revisions` and `unused-revision-names` in {path}"
75 ));
76 }
77
78 let mut bad_revisions = mentioned_revisions
81 .into_iter()
82 .filter(|(rev, _)| !revisions.contains(rev) && !unused_revision_names.contains(rev))
83 .map(|(rev, line_number)| (line_number, rev))
84 .collect::<Vec<_>>();
85 bad_revisions.sort();
86
87 for (line_number, rev) in bad_revisions {
88 check.error(format!("unknown revision [{rev}] at {path}:{line_number}"));
89 }
90}
91
92struct ErrorAnnRev<'a> {
93 line_number: usize,
94 revision: &'a str,
95}
96
97fn for_each_error_annotation_revision<'a>(
98 contents: &'a str,
99 callback: &mut dyn FnMut(ErrorAnnRev<'a>),
100) {
101 let error_regex = {
102 static RE: OnceLock<Regex> = OnceLock::new();
105 RE.get_or_init(|| Regex::new(r"//\[(?<revs>[^]]*)\]~").unwrap())
106 };
107
108 for (line_number, line) in (1..).zip(contents.lines()) {
109 let Some(captures) = error_regex.captures(line) else { continue };
110
111 for revision in captures.name("revs").unwrap().as_str().split(',') {
112 callback(ErrorAnnRev { line_number, revision });
113 }
114 }
115}