lanekeep_core/
query_cover.rs1#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum QueryCoverProblem {
18 NoQueries,
20 Duplicate {
23 language: String,
25 },
26 Missing {
28 language: String,
30 },
31 Undeclared {
33 language: String,
35 },
36}
37
38impl QueryCoverProblem {
39 #[must_use]
45 pub fn describe(&self) -> String {
46 match self {
47 Self::NoQueries => "declares no query for any language — a rule with no query \
48 can never match, and silently"
49 .to_owned(),
50 Self::Duplicate { language } => format!(
51 "declares two queries for `{language}` — only one of them could run, and \
52 the other would be discarded silently"
53 ),
54 Self::Missing { language } => format!(
55 "declares no query for `{language}` — a rule runs only on files whose \
56 language it names, so a language with no query can never match"
57 ),
58 Self::Undeclared { language } => format!(
59 "declares a query for `{language}`, which it does not target — that query \
60 can never run, and nothing would report the mistake"
61 ),
62 }
63 }
64}
65
66pub fn check<'a, I>(languages: &[String], queries: I) -> Result<(), QueryCoverProblem>
77where
78 I: IntoIterator<Item = &'a str>,
79{
80 let entries: Vec<&str> = queries.into_iter().collect();
81
82 if entries.is_empty() {
83 return Err(QueryCoverProblem::NoQueries);
84 }
85
86 let mut seen: Vec<&str> = Vec::with_capacity(entries.len());
87 for language in &entries {
88 if seen.contains(language) {
89 return Err(QueryCoverProblem::Duplicate {
90 language: (*language).to_owned(),
91 });
92 }
93 seen.push(language);
94 }
95
96 for language in languages {
97 if !entries.contains(&language.as_str()) {
98 return Err(QueryCoverProblem::Missing {
99 language: language.clone(),
100 });
101 }
102 }
103
104 for language in &entries {
105 if !languages.iter().any(|declared| declared == language) {
106 return Err(QueryCoverProblem::Undeclared {
107 language: (*language).to_owned(),
108 });
109 }
110 }
111
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn languages(ids: &[&str]) -> Vec<String> {
120 ids.iter().map(|id| (*id).to_owned()).collect()
121 }
122
123 #[test]
124 fn an_exact_cover_passes() {
125 assert_eq!(
126 check(
127 &languages(&["typescript", "python"]),
128 ["typescript", "python"]
129 ),
130 Ok(())
131 );
132 }
133
134 #[test]
135 fn entry_order_does_not_matter_for_a_cover() {
136 assert_eq!(
137 check(
138 &languages(&["typescript", "python"]),
139 ["python", "typescript"]
140 ),
141 Ok(())
142 );
143 }
144
145 #[test]
146 fn no_queries_at_all_is_refused() {
147 assert_eq!(
148 check(&languages(&["rust"]), []),
149 Err(QueryCoverProblem::NoQueries)
150 );
151 }
152
153 #[test]
154 fn a_language_named_twice_is_refused_naming_it() {
155 assert_eq!(
159 check(&languages(&["rust"]), ["rust", "rust"]),
160 Err(QueryCoverProblem::Duplicate {
161 language: "rust".to_owned()
162 })
163 );
164 }
165
166 #[test]
167 fn a_declared_language_without_a_query_is_refused_naming_it() {
168 assert_eq!(
169 check(&languages(&["rust", "go"]), ["rust"]),
170 Err(QueryCoverProblem::Missing {
171 language: "go".to_owned()
172 })
173 );
174 }
175
176 #[test]
177 fn a_query_for_an_undeclared_language_is_refused_naming_it() {
178 assert_eq!(
179 check(&languages(&["rust"]), ["rust", "go"]),
180 Err(QueryCoverProblem::Undeclared {
181 language: "go".to_owned()
182 })
183 );
184 }
185
186 #[test]
187 fn the_first_problem_in_declaration_order_is_the_one_named() {
188 assert_eq!(
190 check(&languages(&["a", "b"]), ["b", "b", "a", "a"]),
191 Err(QueryCoverProblem::Duplicate {
192 language: "b".to_owned()
193 })
194 );
195 }
196
197 #[test]
198 fn every_problem_describes_itself_without_the_id() {
199 for problem in [
202 QueryCoverProblem::NoQueries,
203 QueryCoverProblem::Duplicate {
204 language: "x".to_owned(),
205 },
206 QueryCoverProblem::Missing {
207 language: "x".to_owned(),
208 },
209 QueryCoverProblem::Undeclared {
210 language: "x".to_owned(),
211 },
212 ] {
213 assert!(problem.describe().starts_with("declares"), "{problem:?}");
214 }
215 }
216}