1use std::{
2 collections::{BTreeMap, BTreeSet},
3 convert::TryFrom,
4 sync::LazyLock,
5};
6
7use miette::Diagnostic;
8use thiserror::Error;
9
10use crate::model::{Lint, lint};
11
12#[derive(Debug, Eq, PartialEq, Clone)]
14pub struct Lints {
15 lints: BTreeSet<Lint>,
16}
17
18static AVAILABLE: LazyLock<Lints> = LazyLock::new(|| {
20 let set = Lint::all_lints().collect();
21 Lints::new(set)
22});
23
24impl Lints {
25 #[must_use]
36 pub const fn new(lints: BTreeSet<Lint>) -> Self {
37 Self { lints }
38 }
39
40 #[must_use]
51 pub fn available() -> &'static Self {
52 &AVAILABLE
53 }
54
55 #[must_use]
66 pub fn names(self) -> Vec<&'static str> {
67 self.lints.iter().map(|lint| lint.name()).collect()
68 }
69
70 #[must_use]
81 pub fn config_keys(self) -> Vec<String> {
82 self.lints.iter().map(|lint| lint.config_key()).collect()
83 }
84
85 #[must_use]
97 pub fn merge(&self, other: &Self) -> Self {
98 Self::new(self.lints.union(&other.lints).copied().collect())
99 }
100
101 #[must_use]
113 pub fn subtract(&self, other: &Self) -> Self {
114 Self::new(self.lints.difference(&other.lints).copied().collect())
115 }
116}
117
118impl IntoIterator for Lints {
119 type Item = Lint;
120 type IntoIter = std::collections::btree_set::IntoIter<Lint>;
121
122 fn into_iter(self) -> Self::IntoIter {
123 self.lints.into_iter()
124 }
125}
126
127impl TryFrom<Lints> for String {
128 type Error = Error;
129
130 fn try_from(lints: Lints) -> Result<Self, Self::Error> {
131 let enabled: Vec<_> = lints.into();
132
133 let config: BTreeMap<Self, bool> = Lint::all_lints()
134 .map(|x| (x, enabled.contains(&x)))
135 .fold(BTreeMap::new(), |mut acc, (lint, state)| {
136 acc.insert(lint.to_string(), state);
137 acc
138 });
139
140 let mut inner: BTreeMap<Self, BTreeMap<Self, bool>> = BTreeMap::new();
141 inner.insert("lint".into(), config);
142 let mut output: BTreeMap<Self, BTreeMap<Self, BTreeMap<Self, bool>>> = BTreeMap::new();
143 output.insert("mit".into(), inner);
144
145 Ok(toml::to_string(&output)?)
146 }
147}
148
149impl From<Vec<Lint>> for Lints {
150 fn from(lints: Vec<Lint>) -> Self {
151 Self::new(lints.into_iter().collect())
152 }
153}
154
155impl From<Lints> for Vec<Lint> {
156 fn from(lints: Lints) -> Self {
157 lints.into_iter().collect()
158 }
159}
160
161impl TryFrom<Vec<&str>> for Lints {
162 type Error = Error;
163
164 fn try_from(value: Vec<&str>) -> Result<Self, Self::Error> {
165 let len = value.len();
166 let lints = value
167 .into_iter()
168 .try_fold(
169 Vec::with_capacity(len),
170 |mut lints: Vec<Lint>, item_name| -> Result<Vec<Lint>, Error> {
171 let lint = Lint::try_from(item_name)?;
172 lints.push(lint);
173 Ok(lints)
174 },
175 )?
176 .into_iter();
177
178 Ok(Self::new(lints.collect()))
179 }
180}
181
182#[derive(Error, Debug, Diagnostic)]
184pub enum Error {
185 #[error(transparent)]
187 #[diagnostic(transparent)]
188 LintNameUnknown(#[from] lint::Error),
189 #[error("Failed to parse lint config file: {0}")]
191 #[diagnostic(
192 code(mit_lint::model::lints::error::toml_parse),
193 url(docsrs),
194 help("is it valid toml?")
195 )]
196 TomlParse(#[from] toml::de::Error),
197 #[error("Failed to convert config to toml: {0}")]
199 #[diagnostic(code(mit_lint::model::lints::error::toml_serialize), url(docsrs))]
200 TomlSerialize(#[from] toml::ser::Error),
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 use std::{
208 borrow::Borrow,
209 collections::{BTreeMap, BTreeSet},
210 convert::{TryFrom, TryInto},
211 };
212
213 use quickcheck::TestResult;
214
215 use crate::model::{
216 Lint,
217 lint::Lint::{
218 BodyWiderThan72Characters, DuplicatedTrailers, JiraIssueKeyMissing,
219 PivotalTrackerIdMissing, SubjectLongerThan72Characters, SubjectNotSeparateFromBody,
220 },
221 };
222
223 #[allow(clippy::needless_pass_by_value)]
224 #[quickcheck]
225 fn it_returns_an_error_if_one_of_the_names_is_wrong(lints: Vec<String>) -> TestResult {
226 if lints.is_empty() {
227 return TestResult::discard();
228 }
229
230 let actual: Result<Lints, Error> = lints
231 .iter()
232 .map(Borrow::borrow)
233 .collect::<Vec<&str>>()
234 .try_into();
235
236 TestResult::from_bool(actual.is_err())
237 }
238
239 #[allow(clippy::needless_pass_by_value)]
240 #[quickcheck]
241 fn no_lint_segfaults(lint: Lint, commit: String) -> TestResult {
242 let _ = lint.lint(&commit.into());
243
244 TestResult::passed()
245 }
246
247 #[test]
248 fn example_it_returns_an_error_if_one_of_the_names_is_wrong() {
249 let lints = vec![
250 "pivotal-tracker-id-missing",
251 "broken",
252 "jira-issue-key-missing",
253 ];
254 let actual: Result<Lints, Error> = lints.try_into();
255
256 actual.unwrap_err();
257 }
258
259 #[test]
260 fn test_try_from_preserves_all_lints_from_names() {
261 let all_names: Vec<&str> = Lint::all_lints().map(Lint::name).collect();
265 let result: Lints = all_names
266 .try_into()
267 .expect("All lint names should be valid");
268 let expected = Lints::new(Lint::all_lints().collect());
269 assert_eq!(
270 result, expected,
271 "All lints should round-trip through name parsing"
272 );
273 }
274
275 #[test]
276 fn test_try_from_preserves_duplicate_names_as_unique() {
277 let names = vec![
279 "duplicated-trailers",
280 "duplicated-trailers",
281 "jira-issue-key-missing",
282 ];
283 let result: Lints = names.try_into().expect("Valid lint names should parse");
284 let mut expected_set = BTreeSet::new();
285 expected_set.insert(Lint::DuplicatedTrailers);
286 expected_set.insert(Lint::JiraIssueKeyMissing);
287 assert_eq!(result, Lints::new(expected_set));
288 }
289
290 #[test]
291 fn test_try_from_empty_vec_produces_empty_lints() {
292 let names: Vec<&str> = vec![];
293 let result: Lints = names
294 .try_into()
295 .expect("Empty vec should produce empty Lints");
296 assert!(result.into_iter().next().is_none());
297 }
298
299 #[quickcheck]
300 fn it_can_construct_itself_from_names(lints: Vec<Lint>) -> bool {
301 let lint_names: Vec<&str> = lints.clone().into_iter().map(Lint::name).collect();
302
303 let expected_lints = lints.into_iter().collect::<BTreeSet<Lint>>();
304 let expected = Lints::new(expected_lints);
305
306 let actual: Lints = lint_names.try_into().expect("Lints to have been parsed");
307
308 expected == actual
309 }
310
311 #[test]
312 fn example_it_can_construct_itself_from_names() {
313 let lints = vec!["pivotal-tracker-id-missing", "jira-issue-key-missing"];
314
315 let mut expected_lints = BTreeSet::new();
316 expected_lints.insert(PivotalTrackerIdMissing);
317 expected_lints.insert(JiraIssueKeyMissing);
318
319 let expected = Lints::new(expected_lints);
320 let actual: Lints = lints.try_into().expect("Lints to have been parsed");
321
322 assert_eq!(expected, actual);
323 }
324
325 #[quickcheck]
326 fn it_can_give_me_an_into_iterator(lint_vec: Vec<Lint>) -> bool {
327 let lints = lint_vec.into_iter().collect::<BTreeSet<_>>();
328 let input = Lints::new(lints.clone());
329
330 let expected = lints.into_iter().collect::<Vec<_>>();
331 let actual = input.into_iter().collect::<Vec<_>>();
332
333 expected == actual
334 }
335
336 #[test]
337 fn example_it_can_give_me_an_into_iterator() {
338 let mut lints = BTreeSet::new();
339 lints.insert(PivotalTrackerIdMissing);
340 lints.insert(JiraIssueKeyMissing);
341 let input = Lints::new(lints);
342
343 let expected = vec![PivotalTrackerIdMissing, JiraIssueKeyMissing];
344 let actual = input.into_iter().collect::<Vec<_>>();
345
346 assert_eq!(expected, actual);
347 }
348
349 #[quickcheck]
350 fn it_can_convert_into_a_vec(lint_vec: Vec<Lint>) -> bool {
351 let lints = lint_vec.into_iter().collect::<BTreeSet<_>>();
352 let input = Lints::new(lints.clone());
353
354 let expected = lints.into_iter().collect::<Vec<_>>();
355 let actual: Vec<_> = input.into();
356
357 expected == actual
358 }
359
360 #[test]
361 fn example_it_can_convert_into_a_vec() {
362 let mut lints = BTreeSet::new();
363 lints.insert(PivotalTrackerIdMissing);
364 lints.insert(JiraIssueKeyMissing);
365 let input = Lints::new(lints);
366
367 let expected = vec![PivotalTrackerIdMissing, JiraIssueKeyMissing];
368 let actual: Vec<Lint> = input.into();
369
370 assert_eq!(expected, actual);
371 }
372
373 #[quickcheck]
374 fn it_can_give_me_the_names(lints: BTreeSet<Lint>) -> bool {
375 let lint_names: Vec<&str> = lints.clone().into_iter().map(Lint::name).collect();
376 let actual = Lints::from(lints.into_iter().collect::<Vec<Lint>>()).names();
377
378 lint_names == actual
379 }
380
381 #[test]
382 fn example_it_can_give_me_the_names() {
383 let mut lints = BTreeSet::new();
384 lints.insert(PivotalTrackerIdMissing);
385 lints.insert(JiraIssueKeyMissing);
386 let input = Lints::new(lints);
387
388 let expected = vec![PivotalTrackerIdMissing.name(), JiraIssueKeyMissing.name()];
389 let actual = input.names();
390
391 assert_eq!(expected, actual);
392 }
393
394 #[quickcheck]
395 fn it_can_give_me_the_config_keys(lints: BTreeSet<Lint>) -> bool {
396 let lint_names: Vec<String> = lints.clone().into_iter().map(Lint::config_key).collect();
397 let actual = Lints::from(lints.into_iter().collect::<Vec<Lint>>()).config_keys();
398
399 lint_names == actual
400 }
401
402 #[test]
403 fn example_it_can_give_me_the_config_keys() {
404 let mut lints = BTreeSet::new();
405 lints.insert(PivotalTrackerIdMissing);
406 lints.insert(JiraIssueKeyMissing);
407 let input = Lints::new(lints);
408
409 let expected = vec![
410 PivotalTrackerIdMissing.config_key(),
411 JiraIssueKeyMissing.config_key(),
412 ];
413 let actual = input.config_keys();
414
415 assert_eq!(expected, actual);
416 }
417
418 #[test]
419 fn can_get_all() {
420 let actual = Lints::available();
421 let lints = Lint::all_lints().collect();
422 let expected = &Lints::new(lints);
423
424 assert_eq!(
425 expected, actual,
426 "Expected all the lints to be {expected:?}, instead got {actual:?}"
427 );
428 }
429
430 #[test]
431 fn example_can_get_all() {
432 let actual = Lints::available();
433 let lints = Lint::all_lints().collect();
434 let expected = &Lints::new(lints);
435
436 assert_eq!(
437 expected, actual,
438 "Expected all the lints to be {expected:?}, instead got {actual:?}"
439 );
440 }
441
442 #[allow(clippy::needless_pass_by_value)]
443 #[quickcheck]
444 fn get_toml(expected: BTreeMap<Lint, bool>) -> bool {
445 let toml = String::try_from(Lints::new(
446 expected
447 .iter()
448 .filter(|(_, enabled)| **enabled)
449 .map(|(lint, _)| *lint)
450 .collect(),
451 ))
452 .expect("To be able to convert lints to toml");
453 let full: BTreeMap<String, BTreeMap<String, BTreeMap<String, bool>>> =
454 toml::from_str(toml.as_str()).unwrap();
455 let actual: BTreeMap<Lint, bool> = full
456 .get("mit")
457 .and_then(|x| x.get("lint"))
458 .expect("To have successfully removed the wrapping keys")
459 .iter()
460 .map(|(lint, enabled)| (Lint::try_from(lint.as_str()).unwrap(), *enabled))
461 .collect();
462
463 actual.iter().all(|(actual_key, actual_enabled)| {
464 expected
465 .get(actual_key)
466 .map_or(!*actual_enabled, |expected_enabled| {
467 expected_enabled == actual_enabled
468 })
469 })
470 }
471
472 #[test]
473 fn example_get_toml() {
474 let mut lints_on = BTreeSet::new();
475 lints_on.insert(DuplicatedTrailers);
476 lints_on.insert(SubjectNotSeparateFromBody);
477 lints_on.insert(SubjectLongerThan72Characters);
478 lints_on.insert(BodyWiderThan72Characters);
479 lints_on.insert(PivotalTrackerIdMissing);
480 let actual = String::try_from(Lints::new(lints_on)).expect("Failed to serialise");
481 let expected = "[mit.lint]
482body-wider-than-72-characters = true
483duplicated-trailers = true
484github-id-missing = false
485jira-issue-key-missing = false
486not-conventional-commit = false
487not-emoji-log = false
488pivotal-tracker-id-missing = true
489subject-line-ends-with-period = false
490subject-line-not-capitalized = false
491subject-longer-than-72-characters = true
492subject-not-separated-from-body = true
493";
494
495 assert_eq!(
496 expected, actual,
497 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
498 );
499 }
500
501 #[allow(clippy::needless_pass_by_value)]
502 #[quickcheck]
503 fn two_sets_of_lints_can_be_merged(
504 set_a_lints: BTreeSet<Lint>,
505 set_b_lints: BTreeSet<Lint>,
506 ) -> bool {
507 let set_a = Lints::new(set_a_lints.clone());
508 let set_b = Lints::new(set_b_lints.clone());
509
510 let actual = set_a.merge(&set_b);
511
512 let expected = Lints::new(set_a_lints.union(&set_b_lints).copied().collect());
513
514 expected == actual
515 }
516
517 #[test]
518 fn example_two_sets_of_lints_can_be_merged() {
519 let mut set_a_lints = BTreeSet::new();
520 set_a_lints.insert(PivotalTrackerIdMissing);
521
522 let mut set_b_lints = BTreeSet::new();
523 set_b_lints.insert(DuplicatedTrailers);
524
525 let set_a = Lints::new(set_a_lints);
526 let set_b = Lints::new(set_b_lints);
527
528 let actual = set_a.merge(&set_b);
529
530 let mut lints = BTreeSet::new();
531 lints.insert(DuplicatedTrailers);
532 lints.insert(PivotalTrackerIdMissing);
533 let expected = Lints::new(lints);
534
535 assert_eq!(
536 expected, actual,
537 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
538 );
539 }
540
541 #[allow(clippy::needless_pass_by_value)]
542 #[quickcheck]
543 fn we_can_subtract_lints_from_the_lint_list(
544 set_a_lints: BTreeSet<Lint>,
545 set_b_lints: BTreeSet<Lint>,
546 ) -> bool {
547 let total = Lints::new(set_a_lints.union(&set_b_lints).copied().collect());
548 let set_a = Lints::new(set_a_lints.difference(&set_b_lints).copied().collect());
549 let expected = Lints::new(set_b_lints);
550
551 let actual = total.subtract(&set_a);
552
553 expected == actual
554 }
555
556 #[test]
557 fn example_we_can_subtract_lints_from_the_lint_list() {
558 let mut set_a_lints = BTreeSet::new();
559 set_a_lints.insert(JiraIssueKeyMissing);
560 set_a_lints.insert(PivotalTrackerIdMissing);
561
562 let mut set_b_lints = BTreeSet::new();
563 set_b_lints.insert(DuplicatedTrailers);
564 set_b_lints.insert(PivotalTrackerIdMissing);
565
566 let set_a = Lints::new(set_a_lints);
567 let set_b = Lints::new(set_b_lints);
568
569 let actual = set_a.subtract(&set_b);
570
571 let mut lints = BTreeSet::new();
572 lints.insert(JiraIssueKeyMissing);
573 let expected = Lints::new(lints);
574
575 assert_eq!(
576 expected, actual,
577 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
578 );
579 }
580
581 #[test]
582 fn example_when_merging_overlapping_does_not_lead_to_duplication() {
583 let mut set_a_lints = BTreeSet::new();
584 set_a_lints.insert(PivotalTrackerIdMissing);
585
586 let mut set_b_lints = BTreeSet::new();
587 set_b_lints.insert(DuplicatedTrailers);
588 set_b_lints.insert(PivotalTrackerIdMissing);
589
590 let set_a = Lints::new(set_a_lints);
591 let set_b = Lints::new(set_b_lints);
592
593 let actual = set_a.merge(&set_b);
594
595 let mut lints = BTreeSet::new();
596 lints.insert(DuplicatedTrailers);
597 lints.insert(PivotalTrackerIdMissing);
598 let expected = Lints::new(lints);
599
600 assert_eq!(
601 expected, actual,
602 "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
603 );
604 }
605}