mago_reporting/
lib.rs

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