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    /// Returns the deterministic primary annotation for this issue.
430    ///
431    /// If multiple primary annotations exist, the one with the smallest span is returned.
432    #[must_use]
433    pub fn primary_annotation(&self) -> Option<&Annotation> {
434        self.annotations.iter().filter(|annotation| annotation.is_primary()).min_by_key(|annotation| annotation.span)
435    }
436
437    /// Returns the deterministic primary span for this issue.
438    #[must_use]
439    pub fn primary_span(&self) -> Option<Span> {
440        self.primary_annotation().map(|annotation| annotation.span)
441    }
442
443    /// Add a note to this issue.
444    ///
445    /// # Examples
446    ///
447    /// ```
448    /// use mago_reporting::Issue;
449    ///
450    /// let issue = Issue::error("This is an error").with_note("This is a note");
451    /// ```
452    #[must_use]
453    pub fn with_note(mut self, note: impl Into<String>) -> Self {
454        self.notes.push(note.into());
455
456        self
457    }
458
459    /// Add a help message to this issue.
460    ///
461    /// This is useful for providing additional context to the user on how to resolve the issue.
462    ///
463    /// # Examples
464    ///
465    /// ```
466    /// use mago_reporting::Issue;
467    ///
468    /// let issue = Issue::error("This is an error").with_help("This is a help message");
469    /// ```
470    #[must_use]
471    pub fn with_help(mut self, help: impl Into<String>) -> Self {
472        self.help = Some(help.into());
473
474        self
475    }
476
477    /// Add a link to this issue.
478    ///
479    /// # Examples
480    ///
481    /// ```
482    /// use mago_reporting::Issue;
483    ///
484    /// let issue = Issue::error("This is an error").with_link("https://example.com");
485    /// ```
486    #[must_use]
487    pub fn with_link(mut self, link: impl Into<String>) -> Self {
488        self.link = Some(link.into());
489
490        self
491    }
492
493    /// Add a single edit to this issue.
494    #[must_use]
495    pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
496        self.edits.entry(file_id).or_default().push(edit);
497
498        self
499    }
500
501    /// Add multiple edits to this issue.
502    #[must_use]
503    pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
504        if !edits.is_empty() {
505            self.edits.entry(file_id).or_default().extend(edits);
506        }
507
508        self
509    }
510
511    /// Take the edits from this issue.
512    #[must_use]
513    pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
514        std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
515    }
516}
517
518impl IssueCollection {
519    #[must_use]
520    pub fn new() -> Self {
521        Self { issues: Vec::new() }
522    }
523
524    pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
525        Self { issues: issues.into_iter().collect() }
526    }
527
528    pub fn push(&mut self, issue: Issue) {
529        self.issues.push(issue);
530    }
531
532    pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
533        self.issues.extend(issues);
534    }
535
536    pub fn reserve(&mut self, additional: usize) {
537        self.issues.reserve(additional);
538    }
539
540    pub fn shrink_to_fit(&mut self) {
541        self.issues.shrink_to_fit();
542    }
543
544    #[must_use]
545    pub fn is_empty(&self) -> bool {
546        self.issues.is_empty()
547    }
548
549    #[must_use]
550    pub fn len(&self) -> usize {
551        self.issues.len()
552    }
553
554    /// Filters the issues in the collection to only include those with a severity level
555    /// lower than or equal to the given level.
556    #[must_use]
557    pub fn with_maximum_level(self, level: Level) -> Self {
558        Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
559    }
560
561    /// Filters the issues in the collection to only include those with a severity level
562    ///  higher than or equal to the given level.
563    #[must_use]
564    pub fn with_minimum_level(self, level: Level) -> Self {
565        Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
566    }
567
568    /// Returns `true` if the collection contains any issues with a severity level
569    ///  higher than or equal to the given level.
570    #[must_use]
571    pub fn has_minimum_level(&self, level: Level) -> bool {
572        self.issues.iter().any(|issue| issue.level >= level)
573    }
574
575    /// Returns the number of issues in the collection with the given severity level.
576    #[must_use]
577    pub fn get_level_count(&self, level: Level) -> usize {
578        self.issues.iter().filter(|issue| issue.level == level).count()
579    }
580
581    /// Returns the highest severity level of the issues in the collection.
582    #[must_use]
583    pub fn get_highest_level(&self) -> Option<Level> {
584        self.issues.iter().map(|issue| issue.level).max()
585    }
586
587    /// Returns the lowest severity level of the issues in the collection.
588    #[must_use]
589    pub fn get_lowest_level(&self) -> Option<Level> {
590        self.issues.iter().map(|issue| issue.level).min()
591    }
592
593    pub fn filter_out_ignored<F>(&mut self, ignore: &[IgnoreEntry], resolve_file_name: F)
594    where
595        F: Fn(FileId) -> Option<String>,
596    {
597        if ignore.is_empty() {
598            return;
599        }
600
601        self.issues.retain(|issue| {
602            let Some(code) = &issue.code else {
603                return true;
604            };
605
606            for entry in ignore {
607                match entry {
608                    IgnoreEntry::Code(ignored) if ignored == code => return false,
609                    IgnoreEntry::Scoped { code: ignored, paths } if ignored == code => {
610                        let file_name = issue.primary_span().and_then(|span| resolve_file_name(span.file_id));
611
612                        if let Some(name) = file_name
613                            && is_path_match(&name, paths)
614                        {
615                            return false;
616                        }
617                    }
618                    _ => {}
619                }
620            }
621
622            true
623        });
624    }
625
626    pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
627        self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
628    }
629
630    pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
631        self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
632    }
633
634    /// Filters the issues in the collection to only include those that have associated edits.
635    #[must_use]
636    pub fn with_edits(self) -> Self {
637        Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
638    }
639
640    /// Sorts the issues in the collection.
641    ///
642    /// The issues are sorted by severity level in descending order,
643    /// then by code in ascending order, and finally by the primary annotation span.
644    #[must_use]
645    pub fn sorted(self) -> Self {
646        let mut issues = self.issues;
647
648        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
649            Ordering::Greater => Ordering::Greater,
650            Ordering::Less => Ordering::Less,
651            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
652                Ordering::Less => Ordering::Less,
653                Ordering::Greater => Ordering::Greater,
654                Ordering::Equal => {
655                    let a_span = a.primary_span();
656                    let b_span = b.primary_span();
657
658                    match (a_span, b_span) {
659                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
660                        (Some(_), None) => Ordering::Less,
661                        (None, Some(_)) => Ordering::Greater,
662                        (None, None) => Ordering::Equal,
663                    }
664                }
665            },
666        });
667
668        Self { issues }
669    }
670
671    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
672        self.issues.iter()
673    }
674
675    /// Converts the collection into a map of edit batches grouped by file.
676    ///
677    /// Each batch contains all edits from a single issue along with the rule code.
678    /// All edits from an issue must be applied together as a batch to maintain code validity.
679    ///
680    /// Returns `HashMap<FileId, Vec<(Option<String>, IssueEdits)>>` where each tuple
681    /// is (rule_code, edits_for_that_issue).
682    #[must_use]
683    pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
684        let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
685        for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
686            let code = issue.code;
687            for (file_id, edit_list) in issue.edits {
688                result.entry(file_id).or_default().push((code.clone(), edit_list));
689            }
690        }
691
692        result
693    }
694}
695
696impl IntoIterator for IssueCollection {
697    type Item = Issue;
698
699    type IntoIter = std::vec::IntoIter<Issue>;
700
701    fn into_iter(self) -> Self::IntoIter {
702        self.issues.into_iter()
703    }
704}
705
706impl<'a> IntoIterator for &'a IssueCollection {
707    type Item = &'a Issue;
708
709    type IntoIter = std::slice::Iter<'a, Issue>;
710
711    fn into_iter(self) -> Self::IntoIter {
712        self.issues.iter()
713    }
714}
715
716impl Default for IssueCollection {
717    fn default() -> Self {
718        Self::new()
719    }
720}
721
722impl IntoIterator for Issue {
723    type Item = Issue;
724    type IntoIter = Once<Issue>;
725
726    fn into_iter(self) -> Self::IntoIter {
727        std::iter::once(self)
728    }
729}
730
731impl FromIterator<Issue> for IssueCollection {
732    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
733        Self { issues: iter.into_iter().collect() }
734    }
735}
736
737fn is_path_match(file_name: &str, patterns: &[String]) -> bool {
738    patterns.iter().any(|pattern| {
739        if pattern.ends_with('/') {
740            file_name.starts_with(pattern.as_str())
741        } else {
742            let dir_prefix = format!("{pattern}/");
743            file_name.starts_with(&dir_prefix) || file_name == pattern
744        }
745    })
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    #[test]
753    pub fn test_highest_collection_level() {
754        let mut collection = IssueCollection::from(vec![]);
755        assert_eq!(collection.get_highest_level(), None);
756
757        collection.push(Issue::note("note"));
758        assert_eq!(collection.get_highest_level(), Some(Level::Note));
759
760        collection.push(Issue::help("help"));
761        assert_eq!(collection.get_highest_level(), Some(Level::Help));
762
763        collection.push(Issue::warning("warning"));
764        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
765
766        collection.push(Issue::error("error"));
767        assert_eq!(collection.get_highest_level(), Some(Level::Error));
768    }
769
770    #[test]
771    pub fn test_level_downgrade() {
772        assert_eq!(Level::Error.downgrade(), Level::Warning);
773        assert_eq!(Level::Warning.downgrade(), Level::Help);
774        assert_eq!(Level::Help.downgrade(), Level::Note);
775        assert_eq!(Level::Note.downgrade(), Level::Note);
776    }
777
778    #[test]
779    pub fn test_issue_collection_with_maximum_level() {
780        let mut collection = IssueCollection::from(vec![
781            Issue::error("error"),
782            Issue::warning("warning"),
783            Issue::help("help"),
784            Issue::note("note"),
785        ]);
786
787        collection = collection.with_maximum_level(Level::Warning);
788        assert_eq!(collection.len(), 3);
789        assert_eq!(
790            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
791            vec![Level::Warning, Level::Help, Level::Note]
792        );
793    }
794
795    #[test]
796    pub fn test_issue_collection_with_minimum_level() {
797        let mut collection = IssueCollection::from(vec![
798            Issue::error("error"),
799            Issue::warning("warning"),
800            Issue::help("help"),
801            Issue::note("note"),
802        ]);
803
804        collection = collection.with_minimum_level(Level::Warning);
805        assert_eq!(collection.len(), 2);
806        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
807    }
808
809    #[test]
810    pub fn test_issue_collection_has_minimum_level() {
811        let mut collection = IssueCollection::from(vec![]);
812
813        assert!(!collection.has_minimum_level(Level::Error));
814        assert!(!collection.has_minimum_level(Level::Warning));
815        assert!(!collection.has_minimum_level(Level::Help));
816        assert!(!collection.has_minimum_level(Level::Note));
817
818        collection.push(Issue::note("note"));
819
820        assert!(!collection.has_minimum_level(Level::Error));
821        assert!(!collection.has_minimum_level(Level::Warning));
822        assert!(!collection.has_minimum_level(Level::Help));
823        assert!(collection.has_minimum_level(Level::Note));
824
825        collection.push(Issue::help("help"));
826
827        assert!(!collection.has_minimum_level(Level::Error));
828        assert!(!collection.has_minimum_level(Level::Warning));
829        assert!(collection.has_minimum_level(Level::Help));
830        assert!(collection.has_minimum_level(Level::Note));
831
832        collection.push(Issue::warning("warning"));
833
834        assert!(!collection.has_minimum_level(Level::Error));
835        assert!(collection.has_minimum_level(Level::Warning));
836        assert!(collection.has_minimum_level(Level::Help));
837        assert!(collection.has_minimum_level(Level::Note));
838
839        collection.push(Issue::error("error"));
840
841        assert!(collection.has_minimum_level(Level::Error));
842        assert!(collection.has_minimum_level(Level::Warning));
843        assert!(collection.has_minimum_level(Level::Help));
844        assert!(collection.has_minimum_level(Level::Note));
845    }
846
847    #[test]
848    pub fn test_issue_collection_level_count() {
849        let mut collection = IssueCollection::from(vec![]);
850
851        assert_eq!(collection.get_level_count(Level::Error), 0);
852        assert_eq!(collection.get_level_count(Level::Warning), 0);
853        assert_eq!(collection.get_level_count(Level::Help), 0);
854        assert_eq!(collection.get_level_count(Level::Note), 0);
855
856        collection.push(Issue::error("error"));
857
858        assert_eq!(collection.get_level_count(Level::Error), 1);
859        assert_eq!(collection.get_level_count(Level::Warning), 0);
860        assert_eq!(collection.get_level_count(Level::Help), 0);
861        assert_eq!(collection.get_level_count(Level::Note), 0);
862
863        collection.push(Issue::warning("warning"));
864
865        assert_eq!(collection.get_level_count(Level::Error), 1);
866        assert_eq!(collection.get_level_count(Level::Warning), 1);
867        assert_eq!(collection.get_level_count(Level::Help), 0);
868        assert_eq!(collection.get_level_count(Level::Note), 0);
869
870        collection.push(Issue::help("help"));
871
872        assert_eq!(collection.get_level_count(Level::Error), 1);
873        assert_eq!(collection.get_level_count(Level::Warning), 1);
874        assert_eq!(collection.get_level_count(Level::Help), 1);
875        assert_eq!(collection.get_level_count(Level::Note), 0);
876
877        collection.push(Issue::note("note"));
878
879        assert_eq!(collection.get_level_count(Level::Error), 1);
880        assert_eq!(collection.get_level_count(Level::Warning), 1);
881        assert_eq!(collection.get_level_count(Level::Help), 1);
882        assert_eq!(collection.get_level_count(Level::Note), 1);
883    }
884
885    #[test]
886    pub fn test_primary_span_is_deterministic() {
887        let file = FileId::zero();
888        let span_later = Span::new(file, 20_u32.into(), 25_u32.into());
889        let span_earlier = Span::new(file, 5_u32.into(), 10_u32.into());
890
891        let issue = Issue::error("x")
892            .with_annotation(Annotation::primary(span_later))
893            .with_annotation(Annotation::primary(span_earlier));
894
895        assert_eq!(issue.primary_span(), Some(span_earlier));
896    }
897}