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 filter_out_ignored(&mut self, ignore: &[String]) {
485        self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
486    }
487
488    pub fn take_suggestions(&mut self) -> impl Iterator<Item = (FileId, FixPlan)> + '_ {
489        self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
490    }
491
492    pub fn only_fixable(self) -> impl Iterator<Item = Issue> {
493        self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty())
494    }
495
496    /// Sorts the issues in the collection.
497    ///
498    /// The issues are sorted by severity level in descending order,
499    /// then by code in ascending order, and finally by the primary annotation span.
500    pub fn sorted(self) -> Self {
501        let mut issues = self.issues;
502
503        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
504            Ordering::Greater => Ordering::Greater,
505            Ordering::Less => Ordering::Less,
506            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
507                Ordering::Less => Ordering::Less,
508                Ordering::Greater => Ordering::Greater,
509                Ordering::Equal => {
510                    let a_span = a
511                        .annotations
512                        .iter()
513                        .find(|annotation| annotation.is_primary())
514                        .map(|annotation| annotation.span);
515
516                    let b_span = b
517                        .annotations
518                        .iter()
519                        .find(|annotation| annotation.is_primary())
520                        .map(|annotation| annotation.span);
521
522                    match (a_span, b_span) {
523                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
524                        (Some(_), None) => Ordering::Less,
525                        (None, Some(_)) => Ordering::Greater,
526                        (None, None) => Ordering::Equal,
527                    }
528                }
529            },
530        });
531
532        Self { issues }
533    }
534
535    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
536        self.issues.iter()
537    }
538
539    pub fn to_fix_plans(self) -> HashMap<FileId, FixPlan> {
540        let mut plans: HashMap<FileId, FixPlan> = HashMap::default();
541        for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
542            for suggestion in issue.suggestions.into_iter() {
543                match plans.entry(suggestion.0) {
544                    Entry::Occupied(mut occupied_entry) => {
545                        occupied_entry.get_mut().merge(suggestion.1);
546                    }
547                    Entry::Vacant(vacant_entry) => {
548                        vacant_entry.insert(suggestion.1);
549                    }
550                }
551            }
552        }
553
554        plans
555    }
556}
557
558impl IntoIterator for IssueCollection {
559    type Item = Issue;
560
561    type IntoIter = std::vec::IntoIter<Issue>;
562
563    fn into_iter(self) -> Self::IntoIter {
564        self.issues.into_iter()
565    }
566}
567
568impl Default for IssueCollection {
569    fn default() -> Self {
570        Self::new()
571    }
572}
573
574impl IntoIterator for Issue {
575    type Item = Issue;
576    type IntoIter = Once<Issue>;
577
578    fn into_iter(self) -> Self::IntoIter {
579        std::iter::once(self)
580    }
581}
582
583impl FromIterator<Issue> for IssueCollection {
584    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
585        Self { issues: iter.into_iter().collect() }
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    #[test]
594    pub fn test_highest_collection_level() {
595        let mut collection = IssueCollection::from(vec![]);
596        assert_eq!(collection.get_highest_level(), None);
597
598        collection.push(Issue::note("note"));
599        assert_eq!(collection.get_highest_level(), Some(Level::Note));
600
601        collection.push(Issue::help("help"));
602        assert_eq!(collection.get_highest_level(), Some(Level::Help));
603
604        collection.push(Issue::warning("warning"));
605        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
606
607        collection.push(Issue::error("error"));
608        assert_eq!(collection.get_highest_level(), Some(Level::Error));
609    }
610
611    #[test]
612    pub fn test_level_downgrade() {
613        assert_eq!(Level::Error.downgrade(), Level::Warning);
614        assert_eq!(Level::Warning.downgrade(), Level::Help);
615        assert_eq!(Level::Help.downgrade(), Level::Note);
616        assert_eq!(Level::Note.downgrade(), Level::Note);
617    }
618
619    #[test]
620    pub fn test_issue_collection_with_maximum_level() {
621        let mut collection = IssueCollection::from(vec![
622            Issue::error("error"),
623            Issue::warning("warning"),
624            Issue::help("help"),
625            Issue::note("note"),
626        ]);
627
628        collection = collection.with_maximum_level(Level::Warning);
629        assert_eq!(collection.len(), 3);
630        assert_eq!(
631            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
632            vec![Level::Warning, Level::Help, Level::Note]
633        );
634    }
635
636    #[test]
637    pub fn test_issue_collection_with_minimum_level() {
638        let mut collection = IssueCollection::from(vec![
639            Issue::error("error"),
640            Issue::warning("warning"),
641            Issue::help("help"),
642            Issue::note("note"),
643        ]);
644
645        collection = collection.with_minimum_level(Level::Warning);
646        assert_eq!(collection.len(), 2);
647        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
648    }
649
650    #[test]
651    pub fn test_issue_collection_has_minimum_level() {
652        let mut collection = IssueCollection::from(vec![]);
653
654        assert!(!collection.has_minimum_level(Level::Error));
655        assert!(!collection.has_minimum_level(Level::Warning));
656        assert!(!collection.has_minimum_level(Level::Help));
657        assert!(!collection.has_minimum_level(Level::Note));
658
659        collection.push(Issue::note("note"));
660
661        assert!(!collection.has_minimum_level(Level::Error));
662        assert!(!collection.has_minimum_level(Level::Warning));
663        assert!(!collection.has_minimum_level(Level::Help));
664        assert!(collection.has_minimum_level(Level::Note));
665
666        collection.push(Issue::help("help"));
667
668        assert!(!collection.has_minimum_level(Level::Error));
669        assert!(!collection.has_minimum_level(Level::Warning));
670        assert!(collection.has_minimum_level(Level::Help));
671        assert!(collection.has_minimum_level(Level::Note));
672
673        collection.push(Issue::warning("warning"));
674
675        assert!(!collection.has_minimum_level(Level::Error));
676        assert!(collection.has_minimum_level(Level::Warning));
677        assert!(collection.has_minimum_level(Level::Help));
678        assert!(collection.has_minimum_level(Level::Note));
679
680        collection.push(Issue::error("error"));
681
682        assert!(collection.has_minimum_level(Level::Error));
683        assert!(collection.has_minimum_level(Level::Warning));
684        assert!(collection.has_minimum_level(Level::Help));
685        assert!(collection.has_minimum_level(Level::Note));
686    }
687
688    #[test]
689    pub fn test_issue_collection_level_count() {
690        let mut collection = IssueCollection::from(vec![]);
691
692        assert_eq!(collection.get_level_count(Level::Error), 0);
693        assert_eq!(collection.get_level_count(Level::Warning), 0);
694        assert_eq!(collection.get_level_count(Level::Help), 0);
695        assert_eq!(collection.get_level_count(Level::Note), 0);
696
697        collection.push(Issue::error("error"));
698
699        assert_eq!(collection.get_level_count(Level::Error), 1);
700        assert_eq!(collection.get_level_count(Level::Warning), 0);
701        assert_eq!(collection.get_level_count(Level::Help), 0);
702        assert_eq!(collection.get_level_count(Level::Note), 0);
703
704        collection.push(Issue::warning("warning"));
705
706        assert_eq!(collection.get_level_count(Level::Error), 1);
707        assert_eq!(collection.get_level_count(Level::Warning), 1);
708        assert_eq!(collection.get_level_count(Level::Help), 0);
709        assert_eq!(collection.get_level_count(Level::Note), 0);
710
711        collection.push(Issue::help("help"));
712
713        assert_eq!(collection.get_level_count(Level::Error), 1);
714        assert_eq!(collection.get_level_count(Level::Warning), 1);
715        assert_eq!(collection.get_level_count(Level::Help), 1);
716        assert_eq!(collection.get_level_count(Level::Note), 0);
717
718        collection.push(Issue::note("note"));
719
720        assert_eq!(collection.get_level_count(Level::Error), 1);
721        assert_eq!(collection.get_level_count(Level::Warning), 1);
722        assert_eq!(collection.get_level_count(Level::Help), 1);
723        assert_eq!(collection.get_level_count(Level::Note), 1);
724    }
725}