mit_lint/model/
problem.rs

1use std::fmt::Display;
2
3use miette::{Diagnostic, LabeledSpan, SourceCode};
4use mit_commit::CommitMessage;
5use thiserror::Error;
6
7use crate::model::code::Code;
8
9/// Information about the breaking of the lint
10#[derive(Error, Debug, Eq, PartialEq, Clone)]
11#[error("{error}")]
12pub struct Problem {
13    error: String,
14    tip: String,
15    code: Code,
16    commit_message: String,
17    labels: Option<Vec<(String, usize, usize)>>,
18    url: Option<String>,
19}
20
21impl Diagnostic for Problem {
22    /// Unique diagnostic code that can be used to look up more information
23    /// about this Diagnostic. Ideally also globally unique, and documented in
24    /// the toplevel crate's documentation for easy searching. Rust path
25    /// format (`foo::bar::baz`) is recommended, but more classic codes like
26    /// `E0123` or Enums will work just fine.
27    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
28        Some(Box::new(format!("{:?}", self.code)))
29    }
30
31    /// Additional help text related to this Diagnostic. Do you have any
32    /// advice for the poor soul who's just run into this issue?
33    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
34        Some(Box::new(&self.tip))
35    }
36
37    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
38        self.url
39            .as_deref()
40            .map(|x| Box::new(x) as Box<dyn Display + 'a>)
41    }
42
43    fn source_code(&self) -> Option<&dyn SourceCode> {
44        if self.commit_message.is_empty() {
45            None
46        } else {
47            Some(&self.commit_message)
48        }
49    }
50
51    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
52        if self.commit_message.is_empty() {
53            return None;
54        }
55
56        self.labels.as_ref().map(|labels| {
57            Box::new(
58                labels.iter().map(|(label, offset, len)| {
59                    LabeledSpan::new(Some(label.clone()), *offset, *len)
60                }),
61            ) as Box<dyn Iterator<Item = LabeledSpan> + '_>
62        })
63    }
64}
65
66impl Problem {
67    /// Create a new problem
68    ///
69    /// # Examples
70    ///
71    /// ```rust
72    /// use std::option::Option::None;
73    ///
74    /// use mit_lint::{Code, Problem};
75    /// let problem = Problem::new(
76    ///     "Error title".to_string(),
77    ///     "Some advice on how to fix it".to_string(),
78    ///     Code::BodyWiderThan72Characters,
79    ///     &"Commit Message".into(),
80    ///     None,
81    ///     None,
82    /// );
83    ///
84    /// assert_eq!(problem.error(), "Error title".to_string())
85    /// ```
86    #[must_use]
87    pub fn new(
88        error: String,
89        tip: String,
90        code: Code,
91        commit_message: &CommitMessage<'_>,
92        labels: Option<Vec<(String, usize, usize)>>,
93        url: Option<String>,
94    ) -> Self {
95        Self {
96            error,
97            tip,
98            code,
99            commit_message: String::from(commit_message.clone()),
100            labels,
101            url,
102        }
103    }
104
105    /// Get the code for this problem
106    ///
107    /// # Examples
108    ///
109    /// ```rust
110    /// use std::option::Option::None;
111    ///
112    /// use mit_lint::{Code, Problem};
113    /// let problem = Problem::new(
114    ///     "Error title".to_string(),
115    ///     "Some advice on how to fix it".to_string(),
116    ///     Code::BodyWiderThan72Characters,
117    ///     &"Commit Message".into(),
118    ///     None,
119    ///     None,
120    /// );
121    ///
122    /// assert_eq!(problem.code(), &Code::BodyWiderThan72Characters)
123    /// ```
124    #[must_use]
125    pub const fn code(&self) -> &Code {
126        &self.code
127    }
128
129    /// Get the commit message for this problem
130    ///
131    /// # Examples
132    ///
133    /// ```rust
134    /// use std::option::Option::None;
135    ///
136    /// use mit_commit::CommitMessage;
137    /// use mit_lint::{Code, Problem};
138    /// let problem = Problem::new(
139    ///     "Error title".to_string(),
140    ///     "Some advice on how to fix it".to_string(),
141    ///     Code::BodyWiderThan72Characters,
142    ///     &"Commit Message".into(),
143    ///     None,
144    ///     None,
145    /// );
146    ///
147    /// assert_eq!(
148    ///     problem.commit_message(),
149    ///     CommitMessage::from("Commit Message")
150    /// )
151    /// ```
152    #[must_use]
153    pub fn commit_message(&self) -> CommitMessage<'_> {
154        self.commit_message.clone().into()
155    }
156
157    /// Get the descriptive title for this error
158    ///
159    /// # Examples
160    ///
161    /// ```rust
162    /// use std::option::Option::None;
163    ///
164    /// use mit_lint::{Code, Problem};
165    /// let problem = Problem::new(
166    ///     "Error title".to_string(),
167    ///     "Some advice on how to fix it".to_string(),
168    ///     Code::BodyWiderThan72Characters,
169    ///     &"Commit Message".into(),
170    ///     None,
171    ///     None,
172    /// );
173    ///
174    /// assert_eq!(problem.error(), "Error title".to_string())
175    /// ```
176    #[must_use]
177    pub fn error(&self) -> &str {
178        &self.error
179    }
180
181    /// Get advice on how to fix the problem
182    ///
183    /// This should be a description of why this is a problem, and how to fix it
184    ///
185    /// # Examples
186    ///
187    /// ```rust
188    /// use std::option::Option::None;
189    ///
190    /// use mit_lint::{Code, Problem};
191    /// let problem = Problem::new(
192    ///     "Error title".to_string(),
193    ///     "Some advice on how to fix it".to_string(),
194    ///     Code::BodyWiderThan72Characters,
195    ///     &"Commit Message".into(),
196    ///     None,
197    ///     None,
198    /// );
199    ///
200    /// assert_eq!(problem.tip(), "Some advice on how to fix it".to_string())
201    /// ```
202    #[must_use]
203    pub fn tip(&self) -> &str {
204        &self.tip
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use std::option::Option::None;
212
213    use miette::Diagnostic;
214    use mit_commit::CommitMessage;
215
216    #[test]
217    fn test_error_returns_correct_value() {
218        let problem = Problem::new(
219            "Some error".into(),
220            String::new(),
221            Code::NotConventionalCommit,
222            &"".into(),
223            None,
224            None,
225        );
226        assert_eq!(problem.error(), "Some error");
227    }
228
229    #[test]
230    fn test_empty_commit_returns_no_labels() {
231        let problem = Problem::new(
232            String::new(),
233            String::new(),
234            Code::NotConventionalCommit,
235            &"".into(),
236            Some(vec![("String".to_string(), 10_usize, 20_usize)]),
237            None,
238        );
239        assert!(problem.labels().is_none());
240    }
241
242    #[test]
243    fn test_empty_commit_returns_no_source_code() {
244        let problem = Problem::new(
245            String::new(),
246            String::new(),
247            Code::NotConventionalCommit,
248            &"".into(),
249            Some(vec![("String".to_string(), 10_usize, 20_usize)]),
250            None,
251        );
252        assert!(problem.source_code().is_none());
253    }
254
255    #[allow(
256        clippy::needless_pass_by_value,
257        reason = "Cannot be passed by value, not supported by quickcheck"
258    )]
259    #[quickcheck]
260    fn test_error_matches_input(error: String) -> bool {
261        let problem = Problem::new(
262            error.clone(),
263            String::new(),
264            Code::NotConventionalCommit,
265            &CommitMessage::from(""),
266            None,
267            None,
268        );
269        problem.error() == error
270    }
271
272    #[test]
273    fn test_tip_returns_correct_value() {
274        let problem = Problem::new(
275            String::new(),
276            "Some tip".into(),
277            Code::NotConventionalCommit,
278            &"".into(),
279            None,
280            None,
281        );
282        assert_eq!(problem.tip(), "Some tip");
283    }
284
285    #[allow(
286        clippy::needless_pass_by_value,
287        reason = "Cannot be passed by value, not supported by quickcheck"
288    )]
289    #[quickcheck]
290    fn test_tip_matches_input(tip: String) -> bool {
291        let problem = Problem::new(
292            String::new(),
293            tip.to_string(),
294            Code::NotConventionalCommit,
295            &"".into(),
296            None,
297            None,
298        );
299        problem.tip() == tip
300    }
301
302    #[test]
303    fn test_code_returns_correct_value() {
304        let problem = Problem::new(
305            String::new(),
306            String::new(),
307            Code::NotConventionalCommit,
308            &"".into(),
309            None,
310            None,
311        );
312        assert_eq!(problem.code(), &Code::NotConventionalCommit);
313    }
314
315    #[quickcheck]
316    fn test_code_matches_input(code: Code) {
317        let problem = Problem::new(String::new(), String::new(), code, &"".into(), None, None);
318
319        assert_eq!(problem.code(), &code, "Code should match the input value");
320    }
321
322    #[test]
323    fn test_commit_message_returns_correct_value() {
324        let problem = Problem::new(
325            String::new(),
326            String::new(),
327            Code::NotConventionalCommit,
328            &CommitMessage::from("Commit message"),
329            None,
330            None,
331        );
332        assert_eq!(
333            problem.commit_message(),
334            CommitMessage::from("Commit message")
335        );
336    }
337
338    #[quickcheck]
339    fn test_commit_message_matches_input(message: String) {
340        let problem = Problem::new(
341            String::new(),
342            String::new(),
343            Code::NotConventionalCommit,
344            &CommitMessage::from(message.clone()),
345            None,
346            None,
347        );
348        assert_eq!(
349            problem.commit_message(),
350            CommitMessage::from(message),
351            "Commit message should match the input value"
352        );
353    }
354
355    #[test]
356    fn test_labels_return_correct_values() {
357        let problem = Problem::new(
358            String::new(),
359            String::new(),
360            Code::NotConventionalCommit,
361            &CommitMessage::from("Commit message"),
362            Some(vec![("String".to_string(), 10_usize, 20_usize)]),
363            None,
364        );
365        assert_eq!(
366            problem
367                .labels()
368                .unwrap()
369                .map(|x| (x.label().unwrap().to_string(), x.offset(), x.len()))
370                .collect::<Vec<_>>(),
371            vec![("String".to_string(), 10_usize, 20_usize)]
372        );
373    }
374
375    #[quickcheck]
376    fn test_labels_match_input_values(start: usize, offset: usize) {
377        let problem = Problem::new(
378            String::new(),
379            String::new(),
380            Code::NotConventionalCommit,
381            &CommitMessage::from("Commit message"),
382            Some(vec![("String".to_string(), start, offset)]),
383            None,
384        );
385        assert_eq!(
386            problem
387                .labels()
388                .unwrap()
389                .map(|x| (x.label().unwrap().to_string(), x.offset(), x.len()))
390                .collect::<Vec<_>>(),
391            vec![("String".to_string(), start, offset)]
392        );
393    }
394}