Skip to main content

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