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 shrink_to_fit(&mut self) {
523        self.issues.shrink_to_fit();
524    }
525
526    #[must_use]
527    pub fn is_empty(&self) -> bool {
528        self.issues.is_empty()
529    }
530
531    #[must_use]
532    pub fn len(&self) -> usize {
533        self.issues.len()
534    }
535
536    /// Filters the issues in the collection to only include those with a severity level
537    /// lower than or equal to the given level.
538    #[must_use]
539    pub fn with_maximum_level(self, level: Level) -> Self {
540        Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
541    }
542
543    /// Filters the issues in the collection to only include those with a severity level
544    ///  higher than or equal to the given level.
545    #[must_use]
546    pub fn with_minimum_level(self, level: Level) -> Self {
547        Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
548    }
549
550    /// Returns `true` if the collection contains any issues with a severity level
551    ///  higher than or equal to the given level.
552    #[must_use]
553    pub fn has_minimum_level(&self, level: Level) -> bool {
554        self.issues.iter().any(|issue| issue.level >= level)
555    }
556
557    /// Returns the number of issues in the collection with the given severity level.
558    #[must_use]
559    pub fn get_level_count(&self, level: Level) -> usize {
560        self.issues.iter().filter(|issue| issue.level == level).count()
561    }
562
563    /// Returns the highest severity level of the issues in the collection.
564    #[must_use]
565    pub fn get_highest_level(&self) -> Option<Level> {
566        self.issues.iter().map(|issue| issue.level).max()
567    }
568
569    /// Returns the lowest severity level of the issues in the collection.
570    #[must_use]
571    pub fn get_lowest_level(&self) -> Option<Level> {
572        self.issues.iter().map(|issue| issue.level).min()
573    }
574
575    pub fn filter_out_ignored<F>(&mut self, ignore: &[IgnoreEntry], resolve_file_name: F)
576    where
577        F: Fn(FileId) -> Option<String>,
578    {
579        if ignore.is_empty() {
580            return;
581        }
582
583        self.issues.retain(|issue| {
584            let Some(code) = &issue.code else {
585                return true;
586            };
587
588            for entry in ignore {
589                match entry {
590                    IgnoreEntry::Code(ignored) if ignored == code => return false,
591                    IgnoreEntry::Scoped { code: ignored, paths } if ignored == code => {
592                        let file_name = issue
593                            .annotations
594                            .iter()
595                            .find(|a| a.is_primary())
596                            .and_then(|a| resolve_file_name(a.span.file_id));
597
598                        if let Some(name) = file_name
599                            && is_path_match(&name, paths)
600                        {
601                            return false;
602                        }
603                    }
604                    _ => {}
605                }
606            }
607
608            true
609        });
610    }
611
612    pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
613        self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
614    }
615
616    pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
617        self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
618    }
619
620    /// Filters the issues in the collection to only include those that have associated edits.
621    #[must_use]
622    pub fn with_edits(self) -> Self {
623        Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
624    }
625
626    /// Sorts the issues in the collection.
627    ///
628    /// The issues are sorted by severity level in descending order,
629    /// then by code in ascending order, and finally by the primary annotation span.
630    #[must_use]
631    pub fn sorted(self) -> Self {
632        let mut issues = self.issues;
633
634        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
635            Ordering::Greater => Ordering::Greater,
636            Ordering::Less => Ordering::Less,
637            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
638                Ordering::Less => Ordering::Less,
639                Ordering::Greater => Ordering::Greater,
640                Ordering::Equal => {
641                    let a_span = a
642                        .annotations
643                        .iter()
644                        .find(|annotation| annotation.is_primary())
645                        .map(|annotation| annotation.span);
646
647                    let b_span = b
648                        .annotations
649                        .iter()
650                        .find(|annotation| annotation.is_primary())
651                        .map(|annotation| annotation.span);
652
653                    match (a_span, b_span) {
654                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
655                        (Some(_), None) => Ordering::Less,
656                        (None, Some(_)) => Ordering::Greater,
657                        (None, None) => Ordering::Equal,
658                    }
659                }
660            },
661        });
662
663        Self { issues }
664    }
665
666    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
667        self.issues.iter()
668    }
669
670    /// Converts the collection into a map of edit batches grouped by file.
671    ///
672    /// Each batch contains all edits from a single issue along with the rule code.
673    /// All edits from an issue must be applied together as a batch to maintain code validity.
674    ///
675    /// Returns `HashMap<FileId, Vec<(Option<String>, IssueEdits)>>` where each tuple
676    /// is (rule_code, edits_for_that_issue).
677    #[must_use]
678    pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
679        let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
680        for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
681            let code = issue.code;
682            for (file_id, edit_list) in issue.edits {
683                result.entry(file_id).or_default().push((code.clone(), edit_list));
684            }
685        }
686
687        result
688    }
689}
690
691impl IntoIterator for IssueCollection {
692    type Item = Issue;
693
694    type IntoIter = std::vec::IntoIter<Issue>;
695
696    fn into_iter(self) -> Self::IntoIter {
697        self.issues.into_iter()
698    }
699}
700
701impl<'a> IntoIterator for &'a IssueCollection {
702    type Item = &'a Issue;
703
704    type IntoIter = std::slice::Iter<'a, Issue>;
705
706    fn into_iter(self) -> Self::IntoIter {
707        self.issues.iter()
708    }
709}
710
711impl Default for IssueCollection {
712    fn default() -> Self {
713        Self::new()
714    }
715}
716
717impl IntoIterator for Issue {
718    type Item = Issue;
719    type IntoIter = Once<Issue>;
720
721    fn into_iter(self) -> Self::IntoIter {
722        std::iter::once(self)
723    }
724}
725
726impl FromIterator<Issue> for IssueCollection {
727    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
728        Self { issues: iter.into_iter().collect() }
729    }
730}
731
732fn is_path_match(file_name: &str, patterns: &[String]) -> bool {
733    patterns.iter().any(|pattern| {
734        if pattern.ends_with('/') {
735            file_name.starts_with(pattern.as_str())
736        } else {
737            let dir_prefix = format!("{pattern}/");
738            file_name.starts_with(&dir_prefix) || file_name == pattern
739        }
740    })
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746
747    #[test]
748    pub fn test_highest_collection_level() {
749        let mut collection = IssueCollection::from(vec![]);
750        assert_eq!(collection.get_highest_level(), None);
751
752        collection.push(Issue::note("note"));
753        assert_eq!(collection.get_highest_level(), Some(Level::Note));
754
755        collection.push(Issue::help("help"));
756        assert_eq!(collection.get_highest_level(), Some(Level::Help));
757
758        collection.push(Issue::warning("warning"));
759        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
760
761        collection.push(Issue::error("error"));
762        assert_eq!(collection.get_highest_level(), Some(Level::Error));
763    }
764
765    #[test]
766    pub fn test_level_downgrade() {
767        assert_eq!(Level::Error.downgrade(), Level::Warning);
768        assert_eq!(Level::Warning.downgrade(), Level::Help);
769        assert_eq!(Level::Help.downgrade(), Level::Note);
770        assert_eq!(Level::Note.downgrade(), Level::Note);
771    }
772
773    #[test]
774    pub fn test_issue_collection_with_maximum_level() {
775        let mut collection = IssueCollection::from(vec![
776            Issue::error("error"),
777            Issue::warning("warning"),
778            Issue::help("help"),
779            Issue::note("note"),
780        ]);
781
782        collection = collection.with_maximum_level(Level::Warning);
783        assert_eq!(collection.len(), 3);
784        assert_eq!(
785            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
786            vec![Level::Warning, Level::Help, Level::Note]
787        );
788    }
789
790    #[test]
791    pub fn test_issue_collection_with_minimum_level() {
792        let mut collection = IssueCollection::from(vec![
793            Issue::error("error"),
794            Issue::warning("warning"),
795            Issue::help("help"),
796            Issue::note("note"),
797        ]);
798
799        collection = collection.with_minimum_level(Level::Warning);
800        assert_eq!(collection.len(), 2);
801        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
802    }
803
804    #[test]
805    pub fn test_issue_collection_has_minimum_level() {
806        let mut collection = IssueCollection::from(vec![]);
807
808        assert!(!collection.has_minimum_level(Level::Error));
809        assert!(!collection.has_minimum_level(Level::Warning));
810        assert!(!collection.has_minimum_level(Level::Help));
811        assert!(!collection.has_minimum_level(Level::Note));
812
813        collection.push(Issue::note("note"));
814
815        assert!(!collection.has_minimum_level(Level::Error));
816        assert!(!collection.has_minimum_level(Level::Warning));
817        assert!(!collection.has_minimum_level(Level::Help));
818        assert!(collection.has_minimum_level(Level::Note));
819
820        collection.push(Issue::help("help"));
821
822        assert!(!collection.has_minimum_level(Level::Error));
823        assert!(!collection.has_minimum_level(Level::Warning));
824        assert!(collection.has_minimum_level(Level::Help));
825        assert!(collection.has_minimum_level(Level::Note));
826
827        collection.push(Issue::warning("warning"));
828
829        assert!(!collection.has_minimum_level(Level::Error));
830        assert!(collection.has_minimum_level(Level::Warning));
831        assert!(collection.has_minimum_level(Level::Help));
832        assert!(collection.has_minimum_level(Level::Note));
833
834        collection.push(Issue::error("error"));
835
836        assert!(collection.has_minimum_level(Level::Error));
837        assert!(collection.has_minimum_level(Level::Warning));
838        assert!(collection.has_minimum_level(Level::Help));
839        assert!(collection.has_minimum_level(Level::Note));
840    }
841
842    #[test]
843    pub fn test_issue_collection_level_count() {
844        let mut collection = IssueCollection::from(vec![]);
845
846        assert_eq!(collection.get_level_count(Level::Error), 0);
847        assert_eq!(collection.get_level_count(Level::Warning), 0);
848        assert_eq!(collection.get_level_count(Level::Help), 0);
849        assert_eq!(collection.get_level_count(Level::Note), 0);
850
851        collection.push(Issue::error("error"));
852
853        assert_eq!(collection.get_level_count(Level::Error), 1);
854        assert_eq!(collection.get_level_count(Level::Warning), 0);
855        assert_eq!(collection.get_level_count(Level::Help), 0);
856        assert_eq!(collection.get_level_count(Level::Note), 0);
857
858        collection.push(Issue::warning("warning"));
859
860        assert_eq!(collection.get_level_count(Level::Error), 1);
861        assert_eq!(collection.get_level_count(Level::Warning), 1);
862        assert_eq!(collection.get_level_count(Level::Help), 0);
863        assert_eq!(collection.get_level_count(Level::Note), 0);
864
865        collection.push(Issue::help("help"));
866
867        assert_eq!(collection.get_level_count(Level::Error), 1);
868        assert_eq!(collection.get_level_count(Level::Warning), 1);
869        assert_eq!(collection.get_level_count(Level::Help), 1);
870        assert_eq!(collection.get_level_count(Level::Note), 0);
871
872        collection.push(Issue::note("note"));
873
874        assert_eq!(collection.get_level_count(Level::Error), 1);
875        assert_eq!(collection.get_level_count(Level::Warning), 1);
876        assert_eq!(collection.get_level_count(Level::Help), 1);
877        assert_eq!(collection.get_level_count(Level::Note), 1);
878    }
879}