mit_lint/model/
lints.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    convert::TryFrom,
4    sync::LazyLock,
5    vec::IntoIter,
6};
7
8use miette::Diagnostic;
9use thiserror::Error;
10
11use crate::model::{Lint, lint};
12
13/// A collection of lints
14#[derive(Debug, Eq, PartialEq, Clone)]
15pub struct Lints {
16    lints: BTreeSet<Lint>,
17}
18
19/// All the available lints
20static AVAILABLE: LazyLock<Lints> = LazyLock::new(|| {
21    let set = Lint::all_lints().collect();
22    Lints::new(set)
23});
24
25impl Lints {
26    /// Create a new lint
27    ///
28    /// # Examples
29    ///
30    /// ```rust
31    /// use std::collections::BTreeSet;
32    ///
33    /// use mit_lint::Lints;
34    /// Lints::new(BTreeSet::new());
35    /// ```
36    #[must_use]
37    pub const fn new(lints: BTreeSet<Lint>) -> Self {
38        Self { lints }
39    }
40
41    /// Get the available lints
42    ///
43    /// # Examples
44    ///
45    /// ```rust
46    /// use mit_lint::{Lint, Lints};
47    ///
48    /// let lints = Lints::available().clone();
49    /// assert!(lints.into_iter().count() > 0);
50    /// ```
51    #[must_use]
52    pub fn available() -> &'static Self {
53        &AVAILABLE
54    }
55
56    /// Get all the names of these lints
57    ///
58    /// # Examples
59    ///
60    /// ```rust
61    /// use mit_lint::{Lint, Lints};
62    ///
63    /// let names = Lints::available().clone().names();
64    /// assert!(names.contains(&Lint::SubjectNotSeparateFromBody.name()));
65    /// ```
66    #[must_use]
67    pub fn names(self) -> Vec<&'static str> {
68        self.lints.iter().map(|lint| lint.name()).collect()
69    }
70
71    /// Get all the config keys of these lints
72    ///
73    /// # Examples
74    ///
75    /// ```rust
76    /// use mit_lint::{Lint, Lints};
77    ///
78    /// let names = Lints::available().clone().config_keys();
79    /// assert!(names.contains(&Lint::SubjectNotSeparateFromBody.config_key()));
80    /// ```
81    #[must_use]
82    pub fn config_keys(self) -> Vec<String> {
83        self.lints.iter().map(|lint| lint.config_key()).collect()
84    }
85
86    /// Create the union of two lints
87    ///
88    /// # Examples
89    ///
90    /// ```rust
91    /// use mit_lint::{Lint, Lints};
92    ///
93    /// let to_add = Lints::new(vec![Lint::NotEmojiLog].into_iter().collect());
94    /// let actual = Lints::available().clone().merge(&to_add).names();
95    /// assert!(actual.contains(&Lint::NotEmojiLog.name()));
96    /// ```
97    #[must_use]
98    pub fn merge(&self, other: &Self) -> Self {
99        Self::new(self.lints.union(&other.lints).copied().collect())
100    }
101
102    /// Get the lints that are in self, but not in other
103    ///
104    /// # Examples
105    ///
106    /// ```rust
107    /// use mit_lint::{Lint, Lints};
108    ///
109    /// let to_remove = Lints::new(vec![Lint::SubjectNotSeparateFromBody].into_iter().collect());
110    /// let actual = Lints::available().clone().subtract(&to_remove).names();
111    /// assert!(!actual.contains(&Lint::SubjectNotSeparateFromBody.name()));
112    /// ```
113    #[must_use]
114    pub fn subtract(&self, other: &Self) -> Self {
115        Self::new(self.lints.difference(&other.lints).copied().collect())
116    }
117}
118
119impl IntoIterator for Lints {
120    type Item = Lint;
121    type IntoIter = IntoIter<Lint>;
122
123    fn into_iter(self) -> Self::IntoIter {
124        self.lints.into_iter().collect::<Vec<_>>().into_iter()
125    }
126}
127
128impl TryFrom<Lints> for String {
129    type Error = Error;
130
131    fn try_from(lints: Lints) -> Result<Self, Self::Error> {
132        let enabled: Vec<_> = lints.into();
133
134        let config: BTreeMap<Self, bool> = Lint::all_lints()
135            .map(|x| (x, enabled.contains(&x)))
136            .fold(BTreeMap::new(), |mut acc, (lint, state)| {
137                acc.insert(lint.to_string(), state);
138                acc
139            });
140
141        let mut inner: BTreeMap<Self, BTreeMap<Self, bool>> = BTreeMap::new();
142        inner.insert("lint".into(), config);
143        let mut output: BTreeMap<Self, BTreeMap<Self, BTreeMap<Self, bool>>> = BTreeMap::new();
144        output.insert("mit".into(), inner);
145
146        Ok(toml::to_string(&output)?)
147    }
148}
149
150impl From<Vec<Lint>> for Lints {
151    fn from(lints: Vec<Lint>) -> Self {
152        Self::new(lints.into_iter().collect())
153    }
154}
155
156impl From<Lints> for Vec<Lint> {
157    fn from(lints: Lints) -> Self {
158        lints.into_iter().collect()
159    }
160}
161
162impl TryFrom<Vec<&str>> for Lints {
163    type Error = Error;
164
165    fn try_from(value: Vec<&str>) -> Result<Self, Self::Error> {
166        let lints = value
167            .into_iter()
168            .try_fold(
169                vec![],
170                |lints: Vec<Lint>, item_name| -> Result<Vec<Lint>, Error> {
171                    let lint = Lint::try_from(item_name)?;
172
173                    Ok([lints, vec![lint]].concat())
174                },
175            )
176            .map(Vec::into_iter)?;
177
178        Ok(Self::new(lints.collect()))
179    }
180}
181
182/// General lint related errors
183#[derive(Error, Debug, Diagnostic)]
184pub enum Error {
185    /// Lint name unknown
186    #[error(transparent)]
187    #[diagnostic(transparent)]
188    LintNameUnknown(#[from] lint::Error),
189    /// Failed to parse lint config file
190    #[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    /// Failed to convert config to toml
198    #[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    #[allow(unused_must_use)]
241    #[quickcheck]
242    fn no_lint_segfaults(lint: Lint, commit: String) -> TestResult {
243        lint.lint(&commit.into());
244
245        TestResult::passed()
246    }
247
248    #[test]
249    fn example_it_returns_an_error_if_one_of_the_names_is_wrong() {
250        let lints = vec![
251            "pivotal-tracker-id-missing",
252            "broken",
253            "jira-issue-key-missing",
254        ];
255        let actual: Result<Lints, Error> = lints.try_into();
256
257        actual.unwrap_err();
258    }
259
260    #[quickcheck]
261    fn it_can_construct_itself_from_names(lints: Vec<Lint>) -> bool {
262        let lint_names: Vec<&str> = lints.clone().into_iter().map(Lint::name).collect();
263
264        let expected_lints = lints.into_iter().collect::<BTreeSet<Lint>>();
265        let expected = Lints::new(expected_lints);
266
267        let actual: Lints = lint_names.try_into().expect("Lints to have been parsed");
268
269        expected == actual
270    }
271
272    #[test]
273    fn example_it_can_construct_itself_from_names() {
274        let lints = vec!["pivotal-tracker-id-missing", "jira-issue-key-missing"];
275
276        let mut expected_lints = BTreeSet::new();
277        expected_lints.insert(PivotalTrackerIdMissing);
278        expected_lints.insert(JiraIssueKeyMissing);
279
280        let expected = Lints::new(expected_lints);
281        let actual: Lints = lints.try_into().expect("Lints to have been parsed");
282
283        assert_eq!(expected, actual);
284    }
285
286    #[quickcheck]
287    fn it_can_give_me_an_into_iterator(lint_vec: Vec<Lint>) -> bool {
288        let lints = lint_vec.into_iter().collect::<BTreeSet<_>>();
289        let input = Lints::new(lints.clone());
290
291        let expected = lints.into_iter().collect::<Vec<_>>();
292        let actual = input.into_iter().collect::<Vec<_>>();
293
294        expected == actual
295    }
296
297    #[test]
298    fn example_it_can_give_me_an_into_iterator() {
299        let mut lints = BTreeSet::new();
300        lints.insert(PivotalTrackerIdMissing);
301        lints.insert(JiraIssueKeyMissing);
302        let input = Lints::new(lints);
303
304        let expected = vec![PivotalTrackerIdMissing, JiraIssueKeyMissing];
305        let actual = input.into_iter().collect::<Vec<_>>();
306
307        assert_eq!(expected, actual);
308    }
309
310    #[quickcheck]
311    fn it_can_convert_into_a_vec(lint_vec: Vec<Lint>) -> bool {
312        let lints = lint_vec.into_iter().collect::<BTreeSet<_>>();
313        let input = Lints::new(lints.clone());
314
315        let expected = lints.into_iter().collect::<Vec<_>>();
316        let actual: Vec<_> = input.into();
317
318        expected == actual
319    }
320
321    #[test]
322    fn example_it_can_convert_into_a_vec() {
323        let mut lints = BTreeSet::new();
324        lints.insert(PivotalTrackerIdMissing);
325        lints.insert(JiraIssueKeyMissing);
326        let input = Lints::new(lints);
327
328        let expected = vec![PivotalTrackerIdMissing, JiraIssueKeyMissing];
329        let actual: Vec<Lint> = input.into();
330
331        assert_eq!(expected, actual);
332    }
333
334    #[quickcheck]
335    fn it_can_give_me_the_names(lints: BTreeSet<Lint>) -> bool {
336        let lint_names: Vec<&str> = lints.clone().into_iter().map(Lint::name).collect();
337        let actual = Lints::from(lints.into_iter().collect::<Vec<Lint>>()).names();
338
339        lint_names == actual
340    }
341
342    #[test]
343    fn example_it_can_give_me_the_names() {
344        let mut lints = BTreeSet::new();
345        lints.insert(PivotalTrackerIdMissing);
346        lints.insert(JiraIssueKeyMissing);
347        let input = Lints::new(lints);
348
349        let expected = vec![PivotalTrackerIdMissing.name(), JiraIssueKeyMissing.name()];
350        let actual = input.names();
351
352        assert_eq!(expected, actual);
353    }
354
355    #[quickcheck]
356    fn it_can_give_me_the_config_keys(lints: BTreeSet<Lint>) -> bool {
357        let lint_names: Vec<String> = lints.clone().into_iter().map(Lint::config_key).collect();
358        let actual = Lints::from(lints.into_iter().collect::<Vec<Lint>>()).config_keys();
359
360        lint_names == actual
361    }
362
363    #[test]
364    fn example_it_can_give_me_the_config_keys() {
365        let mut lints = BTreeSet::new();
366        lints.insert(PivotalTrackerIdMissing);
367        lints.insert(JiraIssueKeyMissing);
368        let input = Lints::new(lints);
369
370        let expected = vec![
371            PivotalTrackerIdMissing.config_key(),
372            JiraIssueKeyMissing.config_key(),
373        ];
374        let actual = input.config_keys();
375
376        assert_eq!(expected, actual);
377    }
378
379    #[test]
380    fn can_get_all() {
381        let actual = Lints::available();
382        let lints = Lint::all_lints().collect();
383        let expected = &Lints::new(lints);
384
385        assert_eq!(
386            expected, actual,
387            "Expected all the lints to be {expected:?}, instead got {actual:?}"
388        );
389    }
390
391    #[test]
392    fn example_can_get_all() {
393        let actual = Lints::available();
394        let lints = Lint::all_lints().collect();
395        let expected = &Lints::new(lints);
396
397        assert_eq!(
398            expected, actual,
399            "Expected all the lints to be {expected:?}, instead got {actual:?}"
400        );
401    }
402
403    #[allow(clippy::needless_pass_by_value)]
404    #[quickcheck]
405    fn get_toml(expected: BTreeMap<Lint, bool>) -> bool {
406        let toml = String::try_from(Lints::new(
407            expected
408                .iter()
409                .filter(|(_, enabled)| **enabled)
410                .map(|(lint, _)| *lint)
411                .collect(),
412        ))
413        .expect("To be able to convert lints to toml");
414        let full: BTreeMap<String, BTreeMap<String, BTreeMap<String, bool>>> =
415            toml::from_str(toml.as_str()).unwrap();
416        let actual: BTreeMap<Lint, bool> = full
417            .get("mit")
418            .and_then(|x| x.get("lint"))
419            .expect("To have successfully removed the wrapping keys")
420            .iter()
421            .map(|(lint, enabled)| (Lint::try_from(lint.as_str()).unwrap(), *enabled))
422            .collect();
423
424        actual.iter().all(|(actual_key, actual_enabled)| {
425            expected
426                .get(actual_key)
427                .map_or(!*actual_enabled, |expected_enabled| {
428                    expected_enabled == actual_enabled
429                })
430        })
431    }
432
433    #[test]
434    fn example_get_toml() {
435        let mut lints_on = BTreeSet::new();
436        lints_on.insert(DuplicatedTrailers);
437        lints_on.insert(SubjectNotSeparateFromBody);
438        lints_on.insert(SubjectLongerThan72Characters);
439        lints_on.insert(BodyWiderThan72Characters);
440        lints_on.insert(PivotalTrackerIdMissing);
441        let actual = String::try_from(Lints::new(lints_on)).expect("Failed to serialise");
442        let expected = "[mit.lint]
443body-wider-than-72-characters = true
444duplicated-trailers = true
445github-id-missing = false
446jira-issue-key-missing = false
447not-conventional-commit = false
448not-emoji-log = false
449pivotal-tracker-id-missing = true
450subject-line-ends-with-period = false
451subject-line-not-capitalized = false
452subject-longer-than-72-characters = true
453subject-not-separated-from-body = true
454";
455
456        assert_eq!(
457            expected, actual,
458            "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
459        );
460    }
461
462    #[allow(clippy::needless_pass_by_value)]
463    #[quickcheck]
464    fn two_sets_of_lints_can_be_merged(
465        set_a_lints: BTreeSet<Lint>,
466        set_b_lints: BTreeSet<Lint>,
467    ) -> bool {
468        let set_a = Lints::new(set_a_lints.clone());
469        let set_b = Lints::new(set_b_lints.clone());
470
471        let actual = set_a.merge(&set_b);
472
473        let expected = Lints::new(set_a_lints.union(&set_b_lints).copied().collect());
474
475        expected == actual
476    }
477
478    #[test]
479    fn example_two_sets_of_lints_can_be_merged() {
480        let mut set_a_lints = BTreeSet::new();
481        set_a_lints.insert(PivotalTrackerIdMissing);
482
483        let mut set_b_lints = BTreeSet::new();
484        set_b_lints.insert(DuplicatedTrailers);
485
486        let set_a = Lints::new(set_a_lints);
487        let set_b = Lints::new(set_b_lints);
488
489        let actual = set_a.merge(&set_b);
490
491        let mut lints = BTreeSet::new();
492        lints.insert(DuplicatedTrailers);
493        lints.insert(PivotalTrackerIdMissing);
494        let expected = Lints::new(lints);
495
496        assert_eq!(
497            expected, actual,
498            "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
499        );
500    }
501
502    #[allow(clippy::needless_pass_by_value)]
503    #[quickcheck]
504    fn we_can_subtract_lints_from_the_lint_list(
505        set_a_lints: BTreeSet<Lint>,
506        set_b_lints: BTreeSet<Lint>,
507    ) -> bool {
508        let total = Lints::new(set_a_lints.union(&set_b_lints).copied().collect());
509        let set_a = Lints::new(set_a_lints.difference(&set_b_lints).copied().collect());
510        let expected = Lints::new(set_b_lints);
511
512        let actual = total.subtract(&set_a);
513
514        expected == actual
515    }
516
517    #[test]
518    fn example_we_can_subtract_lints_from_the_lint_list() {
519        let mut set_a_lints = BTreeSet::new();
520        set_a_lints.insert(JiraIssueKeyMissing);
521        set_a_lints.insert(PivotalTrackerIdMissing);
522
523        let mut set_b_lints = BTreeSet::new();
524        set_b_lints.insert(DuplicatedTrailers);
525        set_b_lints.insert(PivotalTrackerIdMissing);
526
527        let set_a = Lints::new(set_a_lints);
528        let set_b = Lints::new(set_b_lints);
529
530        let actual = set_a.subtract(&set_b);
531
532        let mut lints = BTreeSet::new();
533        lints.insert(JiraIssueKeyMissing);
534        let expected = Lints::new(lints);
535
536        assert_eq!(
537            expected, actual,
538            "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
539        );
540    }
541
542    #[test]
543    fn example_when_merging_overlapping_does_not_lead_to_duplication() {
544        let mut set_a_lints = BTreeSet::new();
545        set_a_lints.insert(PivotalTrackerIdMissing);
546
547        let mut set_b_lints = BTreeSet::new();
548        set_b_lints.insert(DuplicatedTrailers);
549        set_b_lints.insert(PivotalTrackerIdMissing);
550
551        let set_a = Lints::new(set_a_lints);
552        let set_b = Lints::new(set_b_lints);
553
554        let actual = set_a.merge(&set_b);
555
556        let mut lints = BTreeSet::new();
557        lints.insert(DuplicatedTrailers);
558        lints.insert(PivotalTrackerIdMissing);
559        let expected = Lints::new(lints);
560
561        assert_eq!(
562            expected, actual,
563            "Expected the list of lint identifiers to be {expected:?}, instead got {actual:?}"
564        );
565    }
566}