mago_reporting/
lib.rs

1//! Issue reporting and formatting for Mago.
2//!
3//! This crate provides functionality for reporting code issues identified by the linter and analyzer.
4//! It includes support for multiple output formats, baseline filtering, and rich terminal output.
5//!
6//! # Core Types
7//!
8//! - [`Issue`]: Represents a single code issue with severity level, annotations, and optional fixes
9//! - [`IssueCollection`]: A collection of issues with filtering and sorting capabilities
10//! - [`reporter::Reporter`]: Handles formatting and outputting issues in various formats
11//! - [`baseline::Baseline`]: Manages baseline files to filter out known issues
12
13use std::cmp::Ordering;
14use std::iter::Once;
15use std::str::FromStr;
16
17use ahash::HashMap;
18use ahash::HashMapExt;
19use schemars::JsonSchema;
20use serde::Deserialize;
21use serde::Serialize;
22use strum::Display;
23use strum::VariantNames;
24
25use mago_database::file::FileId;
26use mago_span::Span;
27use mago_text_edit::TextEdit;
28
29mod formatter;
30mod internal;
31
32pub mod baseline;
33pub mod color;
34pub mod error;
35pub mod output;
36pub mod reporter;
37
38pub use color::ColorChoice;
39pub use formatter::ReportingFormat;
40pub use output::ReportingTarget;
41
42/// Represents the kind of annotation associated with an issue.
43#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
44pub enum AnnotationKind {
45    /// A primary annotation, typically highlighting the main source of the issue.
46    Primary,
47    /// A secondary annotation, providing additional context or related information.
48    Secondary,
49}
50
51/// An annotation associated with an issue, providing additional context or highlighting specific code spans.
52#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
53pub struct Annotation {
54    /// An optional message associated with the annotation.
55    pub message: Option<String>,
56    /// The kind of annotation.
57    pub kind: AnnotationKind,
58    /// The code span that the annotation refers to.
59    pub span: Span,
60}
61
62/// Represents the severity level of an issue.
63#[derive(
64    Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames, JsonSchema,
65)]
66#[strum(serialize_all = "lowercase")]
67pub enum Level {
68    /// A note, providing additional information or context.
69    #[serde(alias = "note")]
70    Note,
71    /// A help message, suggesting possible solutions or further actions.
72    #[serde(alias = "help")]
73    Help,
74    /// A warning, indicating a potential problem that may need attention.
75    #[serde(alias = "warning", alias = "warn")]
76    Warning,
77    /// An error, indicating a problem that prevents the code from functioning correctly.
78    #[serde(alias = "error", alias = "err")]
79    Error,
80}
81
82impl FromStr for Level {
83    type Err = ();
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        match s.to_lowercase().as_str() {
87            "note" => Ok(Self::Note),
88            "help" => Ok(Self::Help),
89            "warning" => Ok(Self::Warning),
90            "error" => Ok(Self::Error),
91            _ => Err(()),
92        }
93    }
94}
95
96type IssueEdits = Vec<TextEdit>;
97type IssueEditBatches = Vec<(Option<String>, IssueEdits)>;
98
99/// Represents an issue identified in the code.
100#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
101pub struct Issue {
102    /// The severity level of the issue.
103    pub level: Level,
104    /// An optional code associated with the issue.
105    pub code: Option<String>,
106    /// The main message describing the issue.
107    pub message: String,
108    /// Additional notes related to the issue.
109    pub notes: Vec<String>,
110    /// An optional help message suggesting possible solutions or further actions.
111    pub help: Option<String>,
112    /// An optional link to external resources for more information about the issue.
113    pub link: Option<String>,
114    /// Annotations associated with the issue, providing additional context or highlighting specific code spans.
115    pub annotations: Vec<Annotation>,
116    /// Text edits that can be applied to fix the issue, grouped by file.
117    pub edits: HashMap<FileId, IssueEdits>,
118}
119
120/// A collection of issues.
121#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
122pub struct IssueCollection {
123    issues: Vec<Issue>,
124}
125
126impl AnnotationKind {
127    /// Returns `true` if this annotation kind is primary.
128    #[inline]
129    #[must_use]
130    pub const fn is_primary(&self) -> bool {
131        matches!(self, AnnotationKind::Primary)
132    }
133
134    /// Returns `true` if this annotation kind is secondary.
135    #[inline]
136    #[must_use]
137    pub const fn is_secondary(&self) -> bool {
138        matches!(self, AnnotationKind::Secondary)
139    }
140}
141
142impl Annotation {
143    /// Creates a new annotation with the given kind and span.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use mago_reporting::{Annotation, AnnotationKind};
149    /// use mago_database::file::FileId;
150    /// use mago_span::Span;
151    /// use mago_span::Position;
152    ///
153    /// let file = FileId::zero();
154    /// let start = Position::new(0);
155    /// let end = Position::new(5);
156    /// let span = Span::new(file, start, end);
157    /// let annotation = Annotation::new(AnnotationKind::Primary, span);
158    /// ```
159    #[must_use]
160    pub fn new(kind: AnnotationKind, span: Span) -> Self {
161        Self { message: None, kind, span }
162    }
163
164    /// Creates a new primary annotation with the given span.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use mago_reporting::{Annotation, AnnotationKind};
170    /// use mago_database::file::FileId;
171    /// use mago_span::Span;
172    /// use mago_span::Position;
173    ///
174    /// let file = FileId::zero();
175    /// let start = Position::new(0);
176    /// let end = Position::new(5);
177    /// let span = Span::new(file, start, end);
178    /// let annotation = Annotation::primary(span);
179    /// ```
180    #[must_use]
181    pub fn primary(span: Span) -> Self {
182        Self::new(AnnotationKind::Primary, span)
183    }
184
185    /// Creates a new secondary annotation with the given span.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// use mago_reporting::{Annotation, AnnotationKind};
191    /// use mago_database::file::FileId;
192    /// use mago_span::Span;
193    /// use mago_span::Position;
194    ///
195    /// let file = FileId::zero();
196    /// let start = Position::new(0);
197    /// let end = Position::new(5);
198    /// let span = Span::new(file, start, end);
199    /// let annotation = Annotation::secondary(span);
200    /// ```
201    #[must_use]
202    pub fn secondary(span: Span) -> Self {
203        Self::new(AnnotationKind::Secondary, span)
204    }
205
206    /// Sets the message of this annotation.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use mago_reporting::{Annotation, AnnotationKind};
212    /// use mago_database::file::FileId;
213    /// use mago_span::Span;
214    /// use mago_span::Position;
215    ///
216    /// let file = FileId::zero();
217    /// let start = Position::new(0);
218    /// let end = Position::new(5);
219    /// let span = Span::new(file, start, end);
220    /// let annotation = Annotation::primary(span).with_message("This is a primary annotation");
221    /// ```
222    #[must_use]
223    pub fn with_message(mut self, message: impl Into<String>) -> Self {
224        self.message = Some(message.into());
225
226        self
227    }
228
229    /// Returns `true` if this annotation is a primary annotation.
230    #[must_use]
231    pub fn is_primary(&self) -> bool {
232        self.kind == AnnotationKind::Primary
233    }
234}
235
236impl Level {
237    /// Downgrades the level to the next lower severity.
238    ///
239    /// This function maps levels to their less severe counterparts:
240    ///
241    /// - `Error` becomes `Warning`
242    /// - `Warning` becomes `Help`
243    /// - `Help` becomes `Note`
244    /// - `Note` remains as `Note`
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use mago_reporting::Level;
250    ///
251    /// let level = Level::Error;
252    /// assert_eq!(level.downgrade(), Level::Warning);
253    ///
254    /// let level = Level::Warning;
255    /// assert_eq!(level.downgrade(), Level::Help);
256    ///
257    /// let level = Level::Help;
258    /// assert_eq!(level.downgrade(), Level::Note);
259    ///
260    /// let level = Level::Note;
261    /// assert_eq!(level.downgrade(), Level::Note);
262    /// ```
263    #[must_use]
264    pub fn downgrade(&self) -> Self {
265        match self {
266            Level::Error => Level::Warning,
267            Level::Warning => Level::Help,
268            Level::Help | Level::Note => Level::Note,
269        }
270    }
271}
272
273impl Issue {
274    /// Creates a new issue with the given level and message.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use mago_reporting::{Issue, Level};
280    ///
281    /// let issue = Issue::new(Level::Error, "This is an error");
282    /// ```
283    pub fn new(level: Level, message: impl Into<String>) -> Self {
284        Self {
285            level,
286            code: None,
287            message: message.into(),
288            annotations: Vec::new(),
289            notes: Vec::new(),
290            help: None,
291            link: None,
292            edits: HashMap::default(),
293        }
294    }
295
296    /// Creates a new error issue with the given message.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use mago_reporting::Issue;
302    ///
303    /// let issue = Issue::error("This is an error");
304    /// ```
305    pub fn error(message: impl Into<String>) -> Self {
306        Self::new(Level::Error, message)
307    }
308
309    /// Creates a new warning issue with the given message.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use mago_reporting::Issue;
315    ///
316    /// let issue = Issue::warning("This is a warning");
317    /// ```
318    pub fn warning(message: impl Into<String>) -> Self {
319        Self::new(Level::Warning, message)
320    }
321
322    /// Creates a new help issue with the given message.
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// use mago_reporting::Issue;
328    ///
329    /// let issue = Issue::help("This is a help message");
330    /// ```
331    pub fn help(message: impl Into<String>) -> Self {
332        Self::new(Level::Help, message)
333    }
334
335    /// Creates a new note issue with the given message.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use mago_reporting::Issue;
341    ///
342    /// let issue = Issue::note("This is a note");
343    /// ```
344    pub fn note(message: impl Into<String>) -> Self {
345        Self::new(Level::Note, message)
346    }
347
348    /// Adds a code to this issue.
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// use mago_reporting::{Issue, Level};
354    ///
355    /// let issue = Issue::error("This is an error").with_code("E0001");
356    /// ```
357    #[must_use]
358    pub fn with_code(mut self, code: impl Into<String>) -> Self {
359        self.code = Some(code.into());
360
361        self
362    }
363
364    /// Add an annotation to this issue.
365    ///
366    /// # Examples
367    ///
368    /// ```
369    /// use mago_reporting::{Issue, Annotation, AnnotationKind};
370    /// use mago_database::file::FileId;
371    /// use mago_span::Span;
372    /// use mago_span::Position;
373    ///
374    /// let file = FileId::zero();
375    /// let start = Position::new(0);
376    /// let end = Position::new(5);
377    /// let span = Span::new(file, start, end);
378    ///
379    /// let issue = Issue::error("This is an error").with_annotation(Annotation::primary(span));
380    /// ```
381    #[must_use]
382    pub fn with_annotation(mut self, annotation: Annotation) -> Self {
383        self.annotations.push(annotation);
384
385        self
386    }
387
388    #[must_use]
389    pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
390        self.annotations.extend(annotation);
391
392        self
393    }
394
395    /// Add a note to this issue.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use mago_reporting::Issue;
401    ///
402    /// let issue = Issue::error("This is an error").with_note("This is a note");
403    /// ```
404    #[must_use]
405    pub fn with_note(mut self, note: impl Into<String>) -> Self {
406        self.notes.push(note.into());
407
408        self
409    }
410
411    /// Add a help message to this issue.
412    ///
413    /// This is useful for providing additional context to the user on how to resolve the issue.
414    ///
415    /// # Examples
416    ///
417    /// ```
418    /// use mago_reporting::Issue;
419    ///
420    /// let issue = Issue::error("This is an error").with_help("This is a help message");
421    /// ```
422    #[must_use]
423    pub fn with_help(mut self, help: impl Into<String>) -> Self {
424        self.help = Some(help.into());
425
426        self
427    }
428
429    /// Add a link to this issue.
430    ///
431    /// # Examples
432    ///
433    /// ```
434    /// use mago_reporting::Issue;
435    ///
436    /// let issue = Issue::error("This is an error").with_link("https://example.com");
437    /// ```
438    #[must_use]
439    pub fn with_link(mut self, link: impl Into<String>) -> Self {
440        self.link = Some(link.into());
441
442        self
443    }
444
445    /// Add a single edit to this issue.
446    #[must_use]
447    pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
448        self.edits.entry(file_id).or_default().push(edit);
449
450        self
451    }
452
453    /// Add multiple edits to this issue.
454    #[must_use]
455    pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
456        if !edits.is_empty() {
457            self.edits.entry(file_id).or_default().extend(edits);
458        }
459
460        self
461    }
462
463    /// Take the edits from this issue.
464    #[must_use]
465    pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
466        std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
467    }
468}
469
470impl IssueCollection {
471    #[must_use]
472    pub fn new() -> Self {
473        Self { issues: Vec::new() }
474    }
475
476    pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
477        Self { issues: issues.into_iter().collect() }
478    }
479
480    pub fn push(&mut self, issue: Issue) {
481        self.issues.push(issue);
482    }
483
484    pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
485        self.issues.extend(issues);
486    }
487
488    pub fn shrink_to_fit(&mut self) {
489        self.issues.shrink_to_fit();
490    }
491
492    #[must_use]
493    pub fn is_empty(&self) -> bool {
494        self.issues.is_empty()
495    }
496
497    #[must_use]
498    pub fn len(&self) -> usize {
499        self.issues.len()
500    }
501
502    /// Filters the issues in the collection to only include those with a severity level
503    /// lower than or equal to the given level.
504    #[must_use]
505    pub fn with_maximum_level(self, level: Level) -> Self {
506        Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
507    }
508
509    /// Filters the issues in the collection to only include those with a severity level
510    ///  higher than or equal to the given level.
511    #[must_use]
512    pub fn with_minimum_level(self, level: Level) -> Self {
513        Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
514    }
515
516    /// Returns `true` if the collection contains any issues with a severity level
517    ///  higher than or equal to the given level.
518    #[must_use]
519    pub fn has_minimum_level(&self, level: Level) -> bool {
520        self.issues.iter().any(|issue| issue.level >= level)
521    }
522
523    /// Returns the number of issues in the collection with the given severity level.
524    #[must_use]
525    pub fn get_level_count(&self, level: Level) -> usize {
526        self.issues.iter().filter(|issue| issue.level == level).count()
527    }
528
529    /// Returns the highest severity level of the issues in the collection.
530    #[must_use]
531    pub fn get_highest_level(&self) -> Option<Level> {
532        self.issues.iter().map(|issue| issue.level).max()
533    }
534
535    /// Returns the lowest severity level of the issues in the collection.
536    #[must_use]
537    pub fn get_lowest_level(&self) -> Option<Level> {
538        self.issues.iter().map(|issue| issue.level).min()
539    }
540
541    pub fn filter_out_ignored(&mut self, ignore: &[String]) {
542        self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
543    }
544
545    pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
546        self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
547    }
548
549    pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
550        self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
551    }
552
553    /// Filters the issues in the collection to only include those that have associated edits.
554    #[must_use]
555    pub fn with_edits(self) -> Self {
556        Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
557    }
558
559    /// Sorts the issues in the collection.
560    ///
561    /// The issues are sorted by severity level in descending order,
562    /// then by code in ascending order, and finally by the primary annotation span.
563    #[must_use]
564    pub fn sorted(self) -> Self {
565        let mut issues = self.issues;
566
567        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
568            Ordering::Greater => Ordering::Greater,
569            Ordering::Less => Ordering::Less,
570            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
571                Ordering::Less => Ordering::Less,
572                Ordering::Greater => Ordering::Greater,
573                Ordering::Equal => {
574                    let a_span = a
575                        .annotations
576                        .iter()
577                        .find(|annotation| annotation.is_primary())
578                        .map(|annotation| annotation.span);
579
580                    let b_span = b
581                        .annotations
582                        .iter()
583                        .find(|annotation| annotation.is_primary())
584                        .map(|annotation| annotation.span);
585
586                    match (a_span, b_span) {
587                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
588                        (Some(_), None) => Ordering::Less,
589                        (None, Some(_)) => Ordering::Greater,
590                        (None, None) => Ordering::Equal,
591                    }
592                }
593            },
594        });
595
596        Self { issues }
597    }
598
599    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
600        self.issues.iter()
601    }
602
603    /// Converts the collection into a map of edit batches grouped by file.
604    ///
605    /// Each batch contains all edits from a single issue along with the rule code.
606    /// All edits from an issue must be applied together as a batch to maintain code validity.
607    ///
608    /// Returns `HashMap<FileId, Vec<(Option<String>, IssueEdits)>>` where each tuple
609    /// is (rule_code, edits_for_that_issue).
610    #[must_use]
611    pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
612        let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
613        for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
614            let code = issue.code;
615            for (file_id, edit_list) in issue.edits {
616                result.entry(file_id).or_default().push((code.clone(), edit_list));
617            }
618        }
619
620        result
621    }
622}
623
624impl IntoIterator for IssueCollection {
625    type Item = Issue;
626
627    type IntoIter = std::vec::IntoIter<Issue>;
628
629    fn into_iter(self) -> Self::IntoIter {
630        self.issues.into_iter()
631    }
632}
633
634impl<'a> IntoIterator for &'a IssueCollection {
635    type Item = &'a Issue;
636
637    type IntoIter = std::slice::Iter<'a, Issue>;
638
639    fn into_iter(self) -> Self::IntoIter {
640        self.issues.iter()
641    }
642}
643
644impl Default for IssueCollection {
645    fn default() -> Self {
646        Self::new()
647    }
648}
649
650impl IntoIterator for Issue {
651    type Item = Issue;
652    type IntoIter = Once<Issue>;
653
654    fn into_iter(self) -> Self::IntoIter {
655        std::iter::once(self)
656    }
657}
658
659impl FromIterator<Issue> for IssueCollection {
660    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
661        Self { issues: iter.into_iter().collect() }
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    #[test]
670    pub fn test_highest_collection_level() {
671        let mut collection = IssueCollection::from(vec![]);
672        assert_eq!(collection.get_highest_level(), None);
673
674        collection.push(Issue::note("note"));
675        assert_eq!(collection.get_highest_level(), Some(Level::Note));
676
677        collection.push(Issue::help("help"));
678        assert_eq!(collection.get_highest_level(), Some(Level::Help));
679
680        collection.push(Issue::warning("warning"));
681        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
682
683        collection.push(Issue::error("error"));
684        assert_eq!(collection.get_highest_level(), Some(Level::Error));
685    }
686
687    #[test]
688    pub fn test_level_downgrade() {
689        assert_eq!(Level::Error.downgrade(), Level::Warning);
690        assert_eq!(Level::Warning.downgrade(), Level::Help);
691        assert_eq!(Level::Help.downgrade(), Level::Note);
692        assert_eq!(Level::Note.downgrade(), Level::Note);
693    }
694
695    #[test]
696    pub fn test_issue_collection_with_maximum_level() {
697        let mut collection = IssueCollection::from(vec![
698            Issue::error("error"),
699            Issue::warning("warning"),
700            Issue::help("help"),
701            Issue::note("note"),
702        ]);
703
704        collection = collection.with_maximum_level(Level::Warning);
705        assert_eq!(collection.len(), 3);
706        assert_eq!(
707            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
708            vec![Level::Warning, Level::Help, Level::Note]
709        );
710    }
711
712    #[test]
713    pub fn test_issue_collection_with_minimum_level() {
714        let mut collection = IssueCollection::from(vec![
715            Issue::error("error"),
716            Issue::warning("warning"),
717            Issue::help("help"),
718            Issue::note("note"),
719        ]);
720
721        collection = collection.with_minimum_level(Level::Warning);
722        assert_eq!(collection.len(), 2);
723        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
724    }
725
726    #[test]
727    pub fn test_issue_collection_has_minimum_level() {
728        let mut collection = IssueCollection::from(vec![]);
729
730        assert!(!collection.has_minimum_level(Level::Error));
731        assert!(!collection.has_minimum_level(Level::Warning));
732        assert!(!collection.has_minimum_level(Level::Help));
733        assert!(!collection.has_minimum_level(Level::Note));
734
735        collection.push(Issue::note("note"));
736
737        assert!(!collection.has_minimum_level(Level::Error));
738        assert!(!collection.has_minimum_level(Level::Warning));
739        assert!(!collection.has_minimum_level(Level::Help));
740        assert!(collection.has_minimum_level(Level::Note));
741
742        collection.push(Issue::help("help"));
743
744        assert!(!collection.has_minimum_level(Level::Error));
745        assert!(!collection.has_minimum_level(Level::Warning));
746        assert!(collection.has_minimum_level(Level::Help));
747        assert!(collection.has_minimum_level(Level::Note));
748
749        collection.push(Issue::warning("warning"));
750
751        assert!(!collection.has_minimum_level(Level::Error));
752        assert!(collection.has_minimum_level(Level::Warning));
753        assert!(collection.has_minimum_level(Level::Help));
754        assert!(collection.has_minimum_level(Level::Note));
755
756        collection.push(Issue::error("error"));
757
758        assert!(collection.has_minimum_level(Level::Error));
759        assert!(collection.has_minimum_level(Level::Warning));
760        assert!(collection.has_minimum_level(Level::Help));
761        assert!(collection.has_minimum_level(Level::Note));
762    }
763
764    #[test]
765    pub fn test_issue_collection_level_count() {
766        let mut collection = IssueCollection::from(vec![]);
767
768        assert_eq!(collection.get_level_count(Level::Error), 0);
769        assert_eq!(collection.get_level_count(Level::Warning), 0);
770        assert_eq!(collection.get_level_count(Level::Help), 0);
771        assert_eq!(collection.get_level_count(Level::Note), 0);
772
773        collection.push(Issue::error("error"));
774
775        assert_eq!(collection.get_level_count(Level::Error), 1);
776        assert_eq!(collection.get_level_count(Level::Warning), 0);
777        assert_eq!(collection.get_level_count(Level::Help), 0);
778        assert_eq!(collection.get_level_count(Level::Note), 0);
779
780        collection.push(Issue::warning("warning"));
781
782        assert_eq!(collection.get_level_count(Level::Error), 1);
783        assert_eq!(collection.get_level_count(Level::Warning), 1);
784        assert_eq!(collection.get_level_count(Level::Help), 0);
785        assert_eq!(collection.get_level_count(Level::Note), 0);
786
787        collection.push(Issue::help("help"));
788
789        assert_eq!(collection.get_level_count(Level::Error), 1);
790        assert_eq!(collection.get_level_count(Level::Warning), 1);
791        assert_eq!(collection.get_level_count(Level::Help), 1);
792        assert_eq!(collection.get_level_count(Level::Note), 0);
793
794        collection.push(Issue::note("note"));
795
796        assert_eq!(collection.get_level_count(Level::Error), 1);
797        assert_eq!(collection.get_level_count(Level::Warning), 1);
798        assert_eq!(collection.get_level_count(Level::Help), 1);
799        assert_eq!(collection.get_level_count(Level::Note), 1);
800    }
801}