#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryCoverProblem {
NoQueries,
Duplicate {
language: String,
},
Missing {
language: String,
},
Undeclared {
language: String,
},
}
impl QueryCoverProblem {
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::NoQueries => "declares no query for any language — a rule with no query \
can never match, and silently"
.to_owned(),
Self::Duplicate { language } => format!(
"declares two queries for `{language}` — only one of them could run, and \
the other would be discarded silently"
),
Self::Missing { language } => format!(
"declares no query for `{language}` — a rule runs only on files whose \
language it names, so a language with no query can never match"
),
Self::Undeclared { language } => format!(
"declares a query for `{language}`, which it does not target — that query \
can never run, and nothing would report the mistake"
),
}
}
}
pub fn check<'a, I>(languages: &[String], queries: I) -> Result<(), QueryCoverProblem>
where
I: IntoIterator<Item = &'a str>,
{
let entries: Vec<&str> = queries.into_iter().collect();
if entries.is_empty() {
return Err(QueryCoverProblem::NoQueries);
}
let mut seen: Vec<&str> = Vec::with_capacity(entries.len());
for language in &entries {
if seen.contains(language) {
return Err(QueryCoverProblem::Duplicate {
language: (*language).to_owned(),
});
}
seen.push(language);
}
for language in languages {
if !entries.contains(&language.as_str()) {
return Err(QueryCoverProblem::Missing {
language: language.clone(),
});
}
}
for language in &entries {
if !languages.iter().any(|declared| declared == language) {
return Err(QueryCoverProblem::Undeclared {
language: (*language).to_owned(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn languages(ids: &[&str]) -> Vec<String> {
ids.iter().map(|id| (*id).to_owned()).collect()
}
#[test]
fn an_exact_cover_passes() {
assert_eq!(
check(
&languages(&["typescript", "python"]),
["typescript", "python"]
),
Ok(())
);
}
#[test]
fn entry_order_does_not_matter_for_a_cover() {
assert_eq!(
check(
&languages(&["typescript", "python"]),
["python", "typescript"]
),
Ok(())
);
}
#[test]
fn no_queries_at_all_is_refused() {
assert_eq!(
check(&languages(&["rust"]), []),
Err(QueryCoverProblem::NoQueries)
);
}
#[test]
fn a_language_named_twice_is_refused_naming_it() {
assert_eq!(
check(&languages(&["rust"]), ["rust", "rust"]),
Err(QueryCoverProblem::Duplicate {
language: "rust".to_owned()
})
);
}
#[test]
fn a_declared_language_without_a_query_is_refused_naming_it() {
assert_eq!(
check(&languages(&["rust", "go"]), ["rust"]),
Err(QueryCoverProblem::Missing {
language: "go".to_owned()
})
);
}
#[test]
fn a_query_for_an_undeclared_language_is_refused_naming_it() {
assert_eq!(
check(&languages(&["rust"]), ["rust", "go"]),
Err(QueryCoverProblem::Undeclared {
language: "go".to_owned()
})
);
}
#[test]
fn the_first_problem_in_declaration_order_is_the_one_named() {
assert_eq!(
check(&languages(&["a", "b"]), ["b", "b", "a", "a"]),
Err(QueryCoverProblem::Duplicate {
language: "b".to_owned()
})
);
}
#[test]
fn every_problem_describes_itself_without_the_id() {
for problem in [
QueryCoverProblem::NoQueries,
QueryCoverProblem::Duplicate {
language: "x".to_owned(),
},
QueryCoverProblem::Missing {
language: "x".to_owned(),
},
QueryCoverProblem::Undeclared {
language: "x".to_owned(),
},
] {
assert!(problem.describe().starts_with("declares"), "{problem:?}");
}
}
}