use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::Path;
use std::sync::OnceLock;
use ignore::DirEntry;
use regex::Regex;
use crate::iter_header::{HeaderLine, iter_header};
use crate::walk::{filter_dirs, filter_not_rust, walk};
pub fn check(tests_path: impl AsRef<Path>, bad: &mut bool) {
walk(
tests_path.as_ref(),
|path, is_dir| {
filter_dirs(path) || filter_not_rust(path) || {
is_dir && path.file_name().is_some_and(|name| name == "auxiliary")
}
},
&mut |entry, contents| visit_test_file(entry, contents, bad),
);
}
fn visit_test_file(entry: &DirEntry, contents: &str, bad: &mut bool) {
let mut revisions = HashSet::new();
let mut unused_revision_names = HashSet::new();
let mut mentioned_revisions = HashMap::<&str, usize>::new();
let mut add_mentioned_revision = |line_number: usize, revision| {
let first_line = mentioned_revisions.entry(revision).or_insert(line_number);
*first_line = (*first_line).min(line_number);
};
iter_header(contents, &mut |HeaderLine { line_number, revision, directive }| {
if let Some(revs) = directive.strip_prefix("revisions:") {
revisions.extend(revs.split_whitespace());
} else if let Some(revs) = directive.strip_prefix("unused-revision-names:") {
unused_revision_names.extend(revs.split_whitespace());
}
if let Some(revision) = revision {
add_mentioned_revision(line_number, revision);
}
});
if unused_revision_names.contains(&"*") {
return;
}
for_each_error_annotation_revision(contents, &mut |ErrorAnnRev { line_number, revision }| {
add_mentioned_revision(line_number, revision);
});
let path = entry.path().display();
for rev in revisions.intersection(&unused_revision_names).copied().collect::<BTreeSet<_>>() {
tidy_error!(
bad,
"revision name [{rev}] appears in both `revisions` and `unused-revision-names` in {path}"
);
}
let mut bad_revisions = mentioned_revisions
.into_iter()
.filter(|(rev, _)| !revisions.contains(rev) && !unused_revision_names.contains(rev))
.map(|(rev, line_number)| (line_number, rev))
.collect::<Vec<_>>();
bad_revisions.sort();
for (line_number, rev) in bad_revisions {
tidy_error!(bad, "unknown revision [{rev}] at {path}:{line_number}");
}
}
struct ErrorAnnRev<'a> {
line_number: usize,
revision: &'a str,
}
fn for_each_error_annotation_revision<'a>(
contents: &'a str,
callback: &mut dyn FnMut(ErrorAnnRev<'a>),
) {
let error_regex = {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"//\[(?<revs>[^]]*)\]~").unwrap())
};
for (line_number, line) in (1..).zip(contents.lines()) {
let Some(captures) = error_regex.captures(line) else { continue };
for revision in captures.name("revs").unwrap().as_str().split(',') {
callback(ErrorAnnRev { line_number, revision });
}
}
}