Skip to main content

mit_lint/model/
lint.rs

1use std::{
2    convert::{TryFrom, TryInto},
3    str::FromStr,
4    sync::LazyLock,
5};
6
7use miette::Diagnostic;
8use mit_commit::CommitMessage;
9use quickcheck::{Arbitrary, Gen};
10use strum_macros::EnumIter;
11use thiserror::Error;
12
13use crate::{
14    checks, model,
15    model::{Lints, Problem},
16};
17
18/// The lints that are supported
19#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Ord, PartialOrd, EnumIter)]
20pub enum Lint {
21    /// Check for duplicated trailers
22    ///
23    /// # Examples
24    ///
25    /// Passing
26    ///
27    /// ```rust
28    /// use mit_commit::CommitMessage;
29    /// use mit_lint::Lint;
30    ///
31    /// let message: &str = "An example commit
32    ///
33    /// This is an example commit without any duplicate trailers
34    /// "
35    /// .into();
36    /// let actual = Lint::DuplicatedTrailers.lint(&CommitMessage::from(message));
37    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
38    /// ```
39    ///
40    /// Erring
41    ///
42    /// ```rust
43    /// use mit_commit::CommitMessage;
44    /// use mit_lint::{Code, Lint, Problem};
45    ///
46    /// let message: &str = "An example commit
47    ///
48    /// This is an example commit without any duplicate trailers
49    ///
50    /// Signed-off-by: Billie Thompson <email@example.com>
51    /// Signed-off-by: Billie Thompson <email@example.com>
52    /// Co-authored-by: Billie Thompson <email@example.com>
53    /// Co-authored-by: Billie Thompson <email@example.com>
54    /// "
55    /// .into();
56    /// let expected = Some(Problem::new(
57    ///     "Your commit message has duplicated trailers".into(),
58    ///     "These are normally added accidentally when you\'re rebasing or amending to a \
59    ///      commit, sometimes in the text editor, but often by git hooks.\n\nYou can fix \
60    ///      this by deleting the duplicated \"Co-authored-by\", \"Signed-off-by\" fields"
61    ///         .into(),
62    ///     Code::DuplicatedTrailers,
63    ///     &message.into(),
64    ///     Some(vec![
65    ///         ("Duplicated `Co-authored-by`".to_string(), 231, 51),
66    ///         ("Duplicated `Signed-off-by`".to_string(), 128, 50),
67    ///     ]),
68    ///     Some(
69    ///         "https://git-scm.com/docs/githooks#_commit_msg"
70    ///             .parse()
71    ///             .unwrap(),
72    ///     ),
73    /// ));
74    /// let actual = Lint::DuplicatedTrailers.lint(&CommitMessage::from(message));
75    /// assert_eq!(
76    ///     actual, expected,
77    ///     "Expected {:?}, found {:?}",
78    ///     expected, actual
79    /// );
80    /// ```
81    DuplicatedTrailers,
82    /// Check for a missing pivotal tracker id
83    ///
84    /// # Examples
85    ///
86    /// Passing
87    ///
88    /// ```rust
89    /// use mit_commit::CommitMessage;
90    /// use mit_lint::Lint;
91    ///
92    /// let message: &str = "An example commit [fixes #12345678]
93    /// "
94    /// .into();
95    /// let actual = Lint::PivotalTrackerIdMissing.lint(&CommitMessage::from(message));
96    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
97    /// ```
98    ///
99    /// Erring
100    ///
101    /// ```rust
102    /// use mit_commit::CommitMessage;
103    /// use mit_lint::{Code, Lint, Problem};
104    ///
105    /// let message: &str = "An example commit
106    ///
107    /// This is an example commit
108    /// "
109    ///
110    /// .into();
111    /// let expected = Some(Problem::new(
112    ///     "Your commit message is missing a Pivotal Tracker ID".into(),
113    ///     "It's important to add the ID because it allows code to be linked back to the stories it was done for, it can provide a chain of custody for code for audit purposes, and it can give future explorers of the codebase insight into the wider organisational need behind the change. We may also use it for automation purposes, like generating changelogs or notification emails.\n\nYou can fix this by adding the Id in one of the styles below to the commit message\n[Delivers #12345678]\n[fixes #12345678]\n[finishes #12345678]\n[#12345884 #12345678]\n[#12345884,#12345678]\n[#12345678],[#12345884]\nThis will address [#12345884]"
114    ///         .into(),
115    ///     Code::PivotalTrackerIdMissing,
116    ///     &message.into(),
117    ///     Some(vec![("No Pivotal Tracker ID".to_string(), 19, 25)]),
118    ///     Some("https://www.pivotaltracker.com/help/api?version=v5#Tracker_Updates_in_SCM_Post_Commit_Hooks".parse().unwrap()),
119    /// ));
120    /// let actual = Lint::PivotalTrackerIdMissing.lint(&CommitMessage::from(message));
121    /// assert_eq!(
122    ///     actual, expected,
123    ///     "Expected {:?}, found {:?}",
124    ///     expected, actual
125    /// );
126    /// ```
127    PivotalTrackerIdMissing,
128    /// Check for a missing jira issue key
129    ///
130    /// # Examples
131    ///
132    /// Passing
133    ///
134    /// ```rust
135    /// use mit_commit::CommitMessage;
136    /// use mit_lint::Lint;
137    ///
138    /// let message: &str = "An example commit
139    ///
140    /// Relates-to: JRA-123
141    /// "
142    /// .into();
143    /// let actual = Lint::JiraIssueKeyMissing.lint(&CommitMessage::from(message));
144    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
145    /// ```
146    ///
147    /// Erring
148    ///
149    /// ```rust
150    /// use mit_commit::CommitMessage;
151    /// use mit_lint::{Code, Lint, Problem};
152    ///
153    /// let message: &str = "An example commit
154    ///
155    /// This is an example commit
156    /// "
157    ///
158    /// .into();
159    /// let expected = Some(Problem::new(
160    ///     "Your commit message is missing a JIRA Issue Key".into(),
161    ///     "It's important to add the issue key because it allows us to link code back to the motivations for doing it, and in some cases provide an audit trail for compliance purposes.\n\nYou can fix this by adding a key like `JRA-123` to the commit message"
162    ///         .into(),
163    ///     Code::JiraIssueKeyMissing,&message.into(),
164    ///     Some(vec![("No JIRA Issue Key".to_string(), 19, 25)]),
165    ///     Some("https://support.atlassian.com/jira-software-cloud/docs/what-is-an-issue/#Workingwithissues-Projectkeys".parse().unwrap()),
166    /// ));
167    /// let actual = Lint::JiraIssueKeyMissing.lint(&CommitMessage::from(message));
168    /// assert_eq!(
169    ///     actual, expected,
170    ///     "Expected {:?}, found {:?}",
171    ///     expected, actual
172    /// );
173    /// ```
174    JiraIssueKeyMissing,
175    /// Check for a missing github id
176    ///
177    /// # Examples
178    ///
179    /// Passing
180    ///
181    /// ```rust
182    /// use mit_commit::CommitMessage;
183    /// use mit_lint::Lint;
184    ///
185    /// let message: &str = "An example commit
186    ///
187    /// Relates-to: AnOrganisation/git-mit#642
188    /// "
189    /// .into();
190    /// let actual = Lint::GitHubIdMissing.lint(&CommitMessage::from(message));
191    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
192    /// ```
193    ///
194    /// Erring
195    ///
196    /// ```rust
197    /// use mit_commit::CommitMessage;
198    /// use mit_lint::{Code, Lint, Problem};
199    ///
200    /// let message: &str = "An example commit
201    ///
202    /// This is an example commit
203    /// "
204    ///
205    /// .into();
206    /// let expected = Some(Problem::new(
207    ///      "Your commit message is missing a GitHub ID".into(),
208    ///     "It's important to add the issue ID because it allows us to link code back to the motivations for doing it, and because we can help people exploring the repository link their issues to specific bits of code.\n\nYou can fix this by adding a ID like the following examples:\n\n#642\nGH-642\nAnUser/git-mit#642\nAnOrganisation/git-mit#642\nfixes #642\n\nBe careful just putting '#642' on a line by itself, as '#' is the default comment character"
209    ///         .into(),
210    ///     Code::GitHubIdMissing,&message.into(),Some(vec![("No GitHub ID".to_string(), 19, 25)]),
211    /// Some("https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/autolinked-references-and-urls#issues-and-pull-requests".parse().unwrap()),
212    /// ));
213    /// let actual = Lint::GitHubIdMissing.lint(&CommitMessage::from(message));
214    /// assert_eq!(
215    ///     actual, expected,
216    ///     "Expected {:?}, found {:?}",
217    ///     expected, actual
218    /// );
219    /// ```
220    GitHubIdMissing,
221    /// Subject not being separated from the body
222    ///
223    /// # Examples
224    ///
225    /// Passing
226    ///
227    /// ```rust
228    /// use mit_commit::CommitMessage;
229    /// use mit_lint::Lint;
230    ///
231    /// let message: &str = "An example commit
232    ///
233    /// Some Body Content
234    /// "
235    /// .into();
236    /// let actual = Lint::SubjectNotSeparateFromBody.lint(&CommitMessage::from(message));
237    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
238    /// ```
239    ///
240    /// Erring
241    ///
242    /// ```rust
243    /// use mit_commit::CommitMessage;
244    /// use mit_lint::{Code, Lint, Problem};
245    ///
246    /// let message: &str = "An example commit
247    /// This is an example commit
248    /// "
249    /// .into();
250    /// let expected = Some(Problem::new(
251    ///       "Your commit message is missing a blank line between the subject and the body".into(),
252    ///     "Most tools that render and parse commit messages, expect commit messages to be in the form of subject and body. This includes git itself in tools like git-format-patch. If you don't include this you may see strange behaviour from git and any related tools.\n\nTo fix this separate subject from body with a blank line"
253    ///         .into(),
254    ///     Code::SubjectNotSeparateFromBody,&message.into(),
255    ///     Some(vec![("Missing blank line".to_string(), 18, 25)]),
256    ///     Some("https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines".parse().unwrap()),
257    /// ));
258    /// let actual = Lint::SubjectNotSeparateFromBody.lint(&CommitMessage::from(message));
259    /// assert_eq!(
260    ///     actual, expected,
261    ///     "Expected {:?}, found {:?}",
262    ///     expected, actual
263    /// );
264    /// ```
265    SubjectNotSeparateFromBody,
266    /// Check for a long subject line
267    ///
268    /// # Examples
269    ///
270    /// Passing
271    ///
272    /// ```rust
273    /// use mit_commit::CommitMessage;
274    /// use mit_lint::Lint;
275    ///
276    /// let message: &str = "An example commit
277    ///
278    /// Some Body Content
279    /// "
280    /// .into();
281    /// let actual = Lint::SubjectLongerThan72Characters.lint(&CommitMessage::from(message));
282    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
283    /// ```
284    ///
285    /// Erring
286    ///
287    /// ```
288    /// use mit_commit::CommitMessage;
289    /// use mit_lint::{Code, Lint, Problem};
290    ///
291    /// let message:String = "x".repeat(73).into();
292    /// let expected = Some(Problem::new(
293    ///       "Your subject is longer than 72 characters".into(),
294    ///     "It's important to keep the subject of the commit less than 72 characters because when you look at the git log, that's where it truncates the message. This means that people won't get the entirety of the information in your commit.\n\nPlease keep the subject line 72 characters or under"
295    ///         .into(),
296    ///     Code::SubjectLongerThan72Characters,&message.clone().into(),
297    ///     Some(vec![("Too long".to_string(), 72, 1)]),
298    ///     Some("https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines".parse().unwrap()),
299    /// ));
300    /// let actual = Lint::SubjectLongerThan72Characters.lint(&CommitMessage::from(message));
301    /// assert_eq!(
302    ///     actual, expected,
303    ///     "Expected {:?}, found {:?}",
304    ///     expected, actual
305    /// );
306    /// ```
307    SubjectLongerThan72Characters,
308    /// Check for a non-capitalised subject
309    ///
310    /// # Examples
311    ///
312    /// Passing
313    ///
314    /// ```rust
315    /// use mit_commit::CommitMessage;
316    /// use mit_lint::Lint;
317    ///
318    /// let message: &str = "An example commit\n".into();
319    /// let actual = Lint::SubjectNotCapitalized.lint(&CommitMessage::from(message));
320    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
321    /// ```
322    ///
323    /// Erring
324    ///
325    /// ```rust
326    /// use mit_commit::CommitMessage;
327    /// use mit_lint::{Code, Lint, Problem};
328    ///
329    /// let message: &str =
330    ///     "an example commit\n"
331    /// .into();
332    /// let expected = Some(
333    ///     Problem::new(
334    ///         "Your commit message is missing a capital letter".into(),
335    ///         "The subject line is a title, and as such should be capitalised.\n\nYou can fix this by capitalising the first character in the subject".into(),
336    ///     Code::SubjectNotCapitalized,&message.into(),
337    ///     Some(vec![("Not capitalised".to_string(), 0, 1)]),
338    ///     Some("https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines".parse().unwrap()),
339    /// )
340    /// );
341    /// let actual = Lint::SubjectNotCapitalized.lint(&CommitMessage::from(message));
342    /// assert_eq!(
343    ///     actual, expected,
344    ///     "Expected {:?}, found {:?}",
345    ///     expected, actual
346    /// );
347    /// ```
348    SubjectNotCapitalized,
349    /// Check for period at the end of the subject
350    ///
351    /// # Examples
352    ///
353    /// Passing
354    ///
355    /// ```rust
356    /// use mit_commit::CommitMessage;
357    /// use mit_lint::Lint;
358    ///
359    /// let message: &str = "An example commit\n".into();
360    /// let actual = Lint::SubjectEndsWithPeriod.lint(&CommitMessage::from(message));
361    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
362    /// ```
363    ///
364    /// Erring
365    ///
366    /// ```rust
367    /// use mit_commit::CommitMessage;
368    /// use mit_lint::{Code, Lint, Problem};
369    ///
370    /// let message: &str =
371    ///     "An example commit.\n".into();
372    /// let expected = Some(
373    /// Problem::new(
374    ///     "Your commit message ends with a period".into(),
375    ///     "It's important to keep your commits short, because we only have a limited number of characters to use (72) before the subject line is truncated. Full stops aren't normally in subject lines, and take up an extra character, so we shouldn't use them in commit message subjects.\n\nYou can fix this by removing the period"
376    ///         .into(),
377    ///     Code::SubjectEndsWithPeriod,&message.into(),
378    ///     Some(vec![("Unneeded period".to_string(), 17, 1)]),
379    ///     Some("https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines".parse().unwrap()),
380    /// )
381    /// );
382    /// let actual = Lint::SubjectEndsWithPeriod.lint(&CommitMessage::from(message));
383    /// assert_eq!(
384    ///     actual, expected,
385    ///     "Expected {:?}, found {:?}",
386    ///     expected, actual
387    /// );
388    /// ```
389    SubjectEndsWithPeriod,
390    /// Check for a long body line
391    ///
392    /// # Examples
393    ///
394    /// Passing
395    ///
396    /// ```rust
397    /// use mit_commit::CommitMessage;
398    /// use mit_lint::Lint;
399    ///
400    /// let message: &str = "An example commit\n\nSome Body Content\n".into();
401    /// let actual = Lint::BodyWiderThan72Characters.lint(&CommitMessage::from(message));
402    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
403    /// ```
404    ///
405    /// Erring
406    ///
407    /// ```rust
408    /// use mit_commit::CommitMessage;
409    /// use mit_lint::{Code, Lint, Problem};
410    ///
411    /// let message:String = ["Subject".to_string(), "x".repeat(73).into()].join("\n\n");
412    /// let expected = Some(Problem::new(
413    ///   "Your commit has a body wider than 72 characters".into(),
414    ///     "It's important to keep the body of the commit narrower than 72 characters because when you look at the git log, that's where it truncates the message. This means that people won't get the entirety of the information in your commit.\n\nYou can fix this by making the lines in your body no more than 72 characters"
415    ///         .into(),
416    ///     Code::BodyWiderThan72Characters,&message.clone().into(),
417    ///     Some(vec![("Too long".parse().unwrap(), 81, 1)]),
418    /// Some("https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines".parse().unwrap())
419    /// ));
420    /// let actual = Lint::BodyWiderThan72Characters.lint(&CommitMessage::from(message));
421    /// assert_eq!(
422    ///     actual, expected,
423    ///     "Expected {:?}, found {:?}",
424    ///     expected, actual
425    /// );
426    /// ```
427    BodyWiderThan72Characters,
428    /// Check for commits following the conventional standard
429    ///
430    /// # Examples
431    ///
432    /// Passing
433    ///
434    /// ```rust
435    /// use mit_commit::CommitMessage;
436    /// use mit_lint::Lint;
437    ///
438    /// let message: &str = "refactor: An example commit\n\nSome Body Content\n".into();
439    /// let actual = Lint::NotConventionalCommit.lint(&CommitMessage::from(message));
440    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
441    /// ```
442    ///
443    /// Erring
444    ///
445    /// ```rust
446    /// use mit_commit::CommitMessage;
447    /// use mit_lint::{Code, Lint, Problem};
448    ///
449    /// let message: &str =
450    ///     "An example commit\n\nSome Body Content\n"
451    /// .into();
452    /// let expected = Some(Problem::new(
453    ///       "Your commit message isn't in conventional style".into(),
454    ///      "It's important to follow the conventional commit style when creating your commit message. By using this style we can automatically calculate the version of software using deployment pipelines, and also generate changelogs and other useful information without human interaction.\n\nYou can fix it by following style\n\n<type>[optional scope]: <description>\n\n[optional body]\n\n[optional footer(s)]"
455    ///         .into(),
456    ///     Code::NotConventionalCommit,&message.into(),Some(vec![("Not conventional".to_string(), 0, 17)]),Some("https://www.conventionalcommits.org/".to_string()),
457    /// ));
458    /// let actual = Lint::NotConventionalCommit.lint(&CommitMessage::from(message));
459    /// assert_eq!(
460    ///     actual, expected,
461    ///     "Expected {:?}, found {:?}",
462    ///     expected, actual
463    /// );
464    /// ```
465    NotConventionalCommit,
466    /// Check for commits following the emoji log standard
467    ///
468    /// # Examples
469    ///
470    /// Passing
471    ///
472    /// ```rust
473    /// use mit_commit::CommitMessage;
474    /// use mit_lint::Lint;
475    ///
476    /// let message: &str = "šŸ“– DOC: An example commit\n\nSome Body Content\n".into();
477    /// let actual = Lint::NotEmojiLog.lint(&CommitMessage::from(message));
478    /// assert!(actual.is_none(), "Expected None, found {:?}", actual);
479    /// ```
480    ///
481    /// Erring
482    ///
483    /// ```rust
484    /// use mit_commit::CommitMessage;
485    /// use mit_lint::{Code, Lint, Problem};
486    ///
487    /// let message: &str =
488    ///     "An example commit\n\nSome Body Content\n"
489    /// .into();
490    /// let expected = Some(
491    /// Problem::new(
492    ///        "Your commit message isn't in emoji log style".into(),
493    ///      "It's important to follow the emoji log style when creating your commit message. By using this style we can automatically generate changelogs.\n\nYou can fix it using one of the prefixes:\n\n\nšŸ“¦ NEW:\nšŸ‘Œ IMPROVE:\nšŸ› FIX:\nšŸ“– DOC:\nšŸš€ RELEASE:\nšŸ¤– TEST:\n‼\u{fe0f} BREAKING:"
494    ///         .into(),
495    ///     Code::NotEmojiLog,&message.into(),Some(vec![("Not emoji log".to_string(), 0, 17)]),Some("https://github.com/ahmadawais/Emoji-Log".to_string()),
496    /// ));
497    /// let actual = Lint::NotEmojiLog.lint(&CommitMessage::from(message));
498    /// assert_eq!(
499    ///     actual, expected,
500    ///     "Expected {:?}, found {:?}",
501    ///     expected, actual
502    /// );
503    /// ```
504    NotEmojiLog,
505    /// Check for a missing gitlab id
506    GitLabIdMissing,
507}
508
509/// The prefix we put in front of the lint when serialising
510pub const CONFIG_KEY_PREFIX: &str = "mit.lint";
511
512impl TryFrom<&str> for Lint {
513    type Error = Error;
514
515    fn try_from(from: &str) -> Result<Self, Self::Error> {
516        Self::all_lints()
517            .find(|lint| lint.name() == from)
518            .ok_or_else(|| Error::new_lint_not_found(from))
519    }
520}
521
522impl From<Lint> for String {
523    fn from(from: Lint) -> Self {
524        format!("{from}")
525    }
526}
527
528impl From<Lint> for &str {
529    /// Get an lint's unique name
530    ///
531    /// # Examples
532    ///
533    /// ```
534    /// use mit_lint::Lint;
535    /// let actual: &str = Lint::NotConventionalCommit.into();
536    /// assert_eq!(actual, Lint::NotConventionalCommit.name());
537    /// ```
538    fn from(lint: Lint) -> Self {
539        lint.name()
540    }
541}
542
543impl Lint {
544    /// Get an lint's unique name
545    #[must_use]
546    pub const fn name(self) -> &'static str {
547        match self {
548            Self::DuplicatedTrailers => checks::duplicate_trailers::CONFIG,
549            Self::PivotalTrackerIdMissing => checks::missing_pivotal_tracker_id::CONFIG,
550            Self::JiraIssueKeyMissing => checks::missing_jira_issue_key::CONFIG,
551            Self::GitHubIdMissing => checks::missing_github_id::CONFIG,
552            Self::GitLabIdMissing => checks::missing_gitlab_id::CONFIG,
553            Self::SubjectNotSeparateFromBody => checks::subject_not_separate_from_body::CONFIG,
554            Self::SubjectLongerThan72Characters => {
555                checks::subject_longer_than_72_characters::CONFIG
556            }
557            Self::SubjectNotCapitalized => checks::subject_not_capitalized::CONFIG,
558            Self::SubjectEndsWithPeriod => checks::subject_line_ends_with_period::CONFIG,
559            Self::BodyWiderThan72Characters => checks::body_wider_than_72_characters::CONFIG,
560            Self::NotConventionalCommit => checks::not_conventional_commit::CONFIG,
561            Self::NotEmojiLog => checks::not_emoji_log::CONFIG,
562        }
563    }
564}
565
566/// All the available lints
567static ALL_LINTS: LazyLock<[Lint; 12]> = LazyLock::new(|| {
568    [
569        Lint::DuplicatedTrailers,
570        Lint::PivotalTrackerIdMissing,
571        Lint::JiraIssueKeyMissing,
572        Lint::GitHubIdMissing,
573        Lint::GitLabIdMissing,
574        Lint::SubjectNotSeparateFromBody,
575        Lint::SubjectLongerThan72Characters,
576        Lint::SubjectNotCapitalized,
577        Lint::SubjectEndsWithPeriod,
578        Lint::BodyWiderThan72Characters,
579        Lint::NotConventionalCommit,
580        Lint::NotEmojiLog,
581    ]
582});
583
584/// The ones that are enabled by default
585static DEFAULT_ENABLED_LINTS: LazyLock<[Lint; 4]> = LazyLock::new(|| {
586    [
587        Lint::DuplicatedTrailers,
588        Lint::SubjectNotSeparateFromBody,
589        Lint::SubjectLongerThan72Characters,
590        Lint::BodyWiderThan72Characters,
591    ]
592});
593
594impl Lint {
595    /// Iterator over all the lints
596    ///
597    /// # Examples
598    ///
599    /// ```rust
600    /// use mit_lint::Lint;
601    /// assert!(Lint::all_lints().next().is_some())
602    /// ```
603    pub fn all_lints() -> impl Iterator<Item = Self> {
604        ALL_LINTS.iter().copied()
605    }
606
607    /// Iterator over all the lints
608    ///
609    /// # Examples
610    ///
611    /// ```rust
612    /// use mit_lint::Lint;
613    /// assert!(Lint::iterator().next().is_some())
614    /// ```
615    #[deprecated(since = "0.1.5", note = "iterator was an unusual name. Use all_lints")]
616    pub fn iterator() -> impl Iterator<Item = Self> {
617        Self::all_lints()
618    }
619
620    /// Check if a lint is enabled by default
621    ///
622    /// # Examples
623    ///
624    /// ```rust
625    /// use mit_lint::Lint;
626    /// assert!(Lint::SubjectNotSeparateFromBody.enabled_by_default());
627    /// assert!(!Lint::NotConventionalCommit.enabled_by_default());
628    /// ```
629    #[must_use]
630    pub fn enabled_by_default(self) -> bool {
631        DEFAULT_ENABLED_LINTS.contains(&self)
632    }
633
634    /// Get a key suitable for a configuration document
635    ///
636    /// # Examples
637    ///
638    /// ```rust
639    /// use mit_lint::Lint;
640    /// assert_eq!(
641    ///     Lint::SubjectNotSeparateFromBody.config_key(),
642    ///     "mit.lint.subject-not-separated-from-body"
643    /// );
644    /// ```
645    #[must_use]
646    pub fn config_key(self) -> String {
647        format!("{CONFIG_KEY_PREFIX}.{self}")
648    }
649
650    /// Run this lint on a commit message
651    ///
652    /// # Examples
653    ///
654    /// ```rust
655    /// use mit_commit::CommitMessage;
656    /// use mit_lint::Lint;
657    /// let actual =
658    ///     Lint::NotConventionalCommit.lint(&CommitMessage::from("An example commit message"));
659    /// assert!(actual.is_some());
660    /// ```
661    #[must_use]
662    pub fn lint(self, commit_message: &CommitMessage<'_>) -> Option<Problem> {
663        match self {
664            Self::DuplicatedTrailers => checks::duplicate_trailers::lint(commit_message),
665            Self::PivotalTrackerIdMissing => {
666                checks::missing_pivotal_tracker_id::lint(commit_message)
667            }
668            Self::JiraIssueKeyMissing => checks::missing_jira_issue_key::lint(commit_message),
669            Self::GitHubIdMissing => checks::missing_github_id::lint(commit_message),
670            Self::GitLabIdMissing => checks::missing_gitlab_id::lint(commit_message),
671            Self::SubjectNotSeparateFromBody => {
672                checks::subject_not_separate_from_body::lint(commit_message)
673            }
674            Self::SubjectLongerThan72Characters => {
675                checks::subject_longer_than_72_characters::lint(commit_message)
676            }
677            Self::SubjectNotCapitalized => checks::subject_not_capitalized::lint(commit_message),
678            Self::SubjectEndsWithPeriod => {
679                checks::subject_line_ends_with_period::lint(commit_message)
680            }
681            Self::BodyWiderThan72Characters => {
682                checks::body_wider_than_72_characters::lint(commit_message)
683            }
684            Self::NotConventionalCommit => checks::not_conventional_commit::lint(commit_message),
685            Self::NotEmojiLog => checks::not_emoji_log::lint(commit_message),
686        }
687    }
688
689    /// Try and convert a list of names into lints
690    ///
691    /// # Examples
692    ///
693    /// ```rust
694    /// use mit_lint::Lint;
695    /// let actual = Lint::from_names(vec!["not-emoji-log", "body-wider-than-72-characters"]);
696    /// assert_eq!(
697    ///     actual.unwrap(),
698    ///     vec![Lint::BodyWiderThan72Characters, Lint::NotEmojiLog]
699    /// );
700    /// ```
701    ///
702    /// # Errors
703    /// If the lint does not exist
704    pub fn from_names(names: Vec<&str>) -> Result<Vec<Self>, model::lints::Error> {
705        let lints: Lints = names.try_into()?;
706        Ok(lints.into_iter().collect())
707    }
708}
709
710impl Arbitrary for Lint {
711    fn arbitrary(g: &mut Gen) -> Self {
712        *g.choose(&ALL_LINTS.iter().copied().collect::<Vec<_>>())
713            .unwrap()
714    }
715
716    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
717        quickcheck::empty_shrinker()
718    }
719}
720
721impl std::fmt::Display for Lint {
722    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723        write!(f, "{}", self.name())
724    }
725}
726
727impl FromStr for Lint {
728    type Err = Error;
729
730    fn from_str(s: &str) -> Result<Self, Self::Err> {
731        Self::try_from(s)
732    }
733}
734
735/// Errors
736#[derive(Error, Debug, Diagnostic)]
737pub enum Error {
738    /// Lint not found
739    #[error("Lint not found: {0}")]
740    #[diagnostic(
741        code(mit_lint::model::lint::error::LintNotFound),
742        url(docsrs),
743        help("check the list of available lints")
744    )]
745    LintNotFound(#[source_code] String, #[label("Not found")] (usize, usize)),
746}
747
748impl Error {
749    fn new_lint_not_found(missing_lint: &str) -> Self {
750        Self::LintNotFound(missing_lint.to_string(), (0, missing_lint.len()))
751    }
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    use std::convert::TryInto;
759
760    #[quickcheck]
761    fn it_is_creatable_from_string(expected: Lint) -> bool {
762        let lint: String = expected.into();
763        expected == lint.parse().unwrap()
764    }
765
766    #[quickcheck]
767    fn it_is_convertible_to_string(expected: Lint) -> bool {
768        let lint: String = expected.into();
769        expected.name() == lint
770    }
771
772    #[quickcheck]
773    fn it_can_be_created_from_string(expected: Lint) -> bool {
774        let lint: Lint = expected.name().try_into().unwrap();
775        expected == lint
776    }
777
778    #[quickcheck]
779    fn it_is_printable(lint: Lint) -> bool {
780        lint.name() == format!("{lint}")
781    }
782
783    #[quickcheck]
784    fn i_can_get_all_the_lints(lint: Lint) -> bool {
785        Lint::all_lints().any(|x| x == lint)
786    }
787
788    #[test]
789    fn example_it_is_convertible_to_string() {
790        let string: String = Lint::PivotalTrackerIdMissing.into();
791        assert_eq!("pivotal-tracker-id-missing".to_string(), string);
792    }
793
794    #[test]
795    fn example_it_can_be_created_from_string() {
796        let lint: Lint = "pivotal-tracker-id-missing".try_into().unwrap();
797        assert_eq!(Lint::PivotalTrackerIdMissing, lint);
798    }
799
800    #[test]
801    fn example_it_is_printable() {
802        assert_eq!(
803            "pivotal-tracker-id-missing",
804            &format!("{}", Lint::PivotalTrackerIdMissing)
805        );
806    }
807
808    #[test]
809    fn example_i_can_get_all_the_lints() {
810        let all: Vec<Lint> = Lint::all_lints().collect();
811        assert_eq!(
812            all,
813            vec![
814                Lint::DuplicatedTrailers,
815                Lint::PivotalTrackerIdMissing,
816                Lint::JiraIssueKeyMissing,
817                Lint::GitHubIdMissing,
818                Lint::GitLabIdMissing,
819                Lint::SubjectNotSeparateFromBody,
820                Lint::SubjectLongerThan72Characters,
821                Lint::SubjectNotCapitalized,
822                Lint::SubjectEndsWithPeriod,
823                Lint::BodyWiderThan72Characters,
824                Lint::NotConventionalCommit,
825                Lint::NotEmojiLog,
826            ]
827        );
828    }
829
830    #[test]
831    fn example_i_can_get_if_a_lint_is_enabled_by_default() {
832        assert!(Lint::DuplicatedTrailers.enabled_by_default());
833        assert!(!Lint::PivotalTrackerIdMissing.enabled_by_default());
834        assert!(!Lint::JiraIssueKeyMissing.enabled_by_default());
835        assert!(Lint::SubjectNotSeparateFromBody.enabled_by_default());
836        assert!(!Lint::GitHubIdMissing.enabled_by_default());
837    }
838}