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