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_fixer::FixPlan;
13use mago_source::SourceIdentifier;
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<(SourceIdentifier, 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_span::Span;
118    /// use mago_span::Position;
119    ///
120    /// let start = Position::dummy(0);
121    /// let end = Position::dummy(5);
122    /// let span = Span::new(start, end);
123    /// let annotation = Annotation::new(AnnotationKind::Primary, span);
124    /// ```
125    pub fn new(kind: AnnotationKind, span: Span) -> Self {
126        Self { message: None, kind, span }
127    }
128
129    /// Creates a new primary annotation with the given span.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use mago_reporting::{Annotation, AnnotationKind};
135    /// use mago_span::Span;
136    /// use mago_span::Position;
137    ///
138    /// let start = Position::dummy(0);
139    /// let end = Position::dummy(5);
140    /// let span = Span::new(start, end);
141    /// let annotation = Annotation::primary(span);
142    /// ```
143    pub fn primary(span: Span) -> Self {
144        Self::new(AnnotationKind::Primary, span)
145    }
146
147    /// Creates a new secondary annotation with the given span.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use mago_reporting::{Annotation, AnnotationKind};
153    /// use mago_span::Span;
154    /// use mago_span::Position;
155    ///
156    /// let start = Position::dummy(0);
157    /// let end = Position::dummy(5);
158    /// let span = Span::new(start, end);
159    /// let annotation = Annotation::secondary(span);
160    /// ```
161    pub fn secondary(span: Span) -> Self {
162        Self::new(AnnotationKind::Secondary, span)
163    }
164
165    /// Sets the message of this annotation.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use mago_reporting::{Annotation, AnnotationKind};
171    /// use mago_span::Span;
172    /// use mago_span::Position;
173    ///
174    /// let start = Position::dummy(0);
175    /// let end = Position::dummy(5);
176    /// let span = Span::new(start, end);
177    /// let annotation = Annotation::primary(span).with_message("This is a primary annotation");
178    /// ```
179    #[must_use]
180    pub fn with_message(mut self, message: impl Into<String>) -> Self {
181        self.message = Some(message.into());
182
183        self
184    }
185
186    /// Returns `true` if this annotation is a primary annotation.
187    pub fn is_primary(&self) -> bool {
188        self.kind == AnnotationKind::Primary
189    }
190}
191
192impl Level {
193    /// Downgrades the level to the next lower severity.
194    ///
195    /// This function maps levels to their less severe counterparts:
196    ///
197    /// - `Error` becomes `Warning`
198    /// - `Warning` becomes `Help`
199    /// - `Help` becomes `Note`
200    /// - `Note` remains as `Note`
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use mago_reporting::Level;
206    ///
207    /// let level = Level::Error;
208    /// assert_eq!(level.downgrade(), Level::Warning);
209    ///
210    /// let level = Level::Warning;
211    /// assert_eq!(level.downgrade(), Level::Help);
212    ///
213    /// let level = Level::Help;
214    /// assert_eq!(level.downgrade(), Level::Note);
215    ///
216    /// let level = Level::Note;
217    /// assert_eq!(level.downgrade(), Level::Note);
218    /// ```
219    pub fn downgrade(&self) -> Self {
220        match self {
221            Level::Error => Level::Warning,
222            Level::Warning => Level::Help,
223            Level::Help | Level::Note => Level::Note,
224        }
225    }
226}
227
228impl Issue {
229    /// Creates a new issue with the given level and message.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use mago_reporting::{Issue, Level};
235    ///
236    /// let issue = Issue::new(Level::Error, "This is an error");
237    /// ```
238    pub fn new(level: Level, message: impl Into<String>) -> Self {
239        Self {
240            level,
241            code: None,
242            message: message.into(),
243            annotations: Vec::new(),
244            notes: Vec::new(),
245            help: None,
246            link: None,
247            suggestions: Vec::new(),
248        }
249    }
250
251    /// Creates a new error issue with the given message.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use mago_reporting::Issue;
257    ///
258    /// let issue = Issue::error("This is an error");
259    /// ```
260    pub fn error(message: impl Into<String>) -> Self {
261        Self::new(Level::Error, message)
262    }
263
264    /// Creates a new warning issue with the given message.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use mago_reporting::Issue;
270    ///
271    /// let issue = Issue::warning("This is a warning");
272    /// ```
273    pub fn warning(message: impl Into<String>) -> Self {
274        Self::new(Level::Warning, message)
275    }
276
277    /// Creates a new help issue with the given message.
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use mago_reporting::Issue;
283    ///
284    /// let issue = Issue::help("This is a help message");
285    /// ```
286    pub fn help(message: impl Into<String>) -> Self {
287        Self::new(Level::Help, message)
288    }
289
290    /// Creates a new note issue with the given message.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use mago_reporting::Issue;
296    ///
297    /// let issue = Issue::note("This is a note");
298    /// ```
299    pub fn note(message: impl Into<String>) -> Self {
300        Self::new(Level::Note, message)
301    }
302
303    /// Adds a code to this issue.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use mago_reporting::{Issue, Level};
309    ///
310    /// let issue = Issue::error("This is an error").with_code("E0001");
311    /// ```
312    #[must_use]
313    pub fn with_code(mut self, code: impl Into<String>) -> Self {
314        self.code = Some(code.into());
315
316        self
317    }
318
319    /// Add an annotation to this issue.
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use mago_reporting::{Issue, Annotation, AnnotationKind};
325    /// use mago_span::Span;
326    /// use mago_span::Position;
327    ///
328    /// let start = Position::dummy(0);
329    /// let end = Position::dummy(5);
330    /// let span = Span::new(start, end);
331    ///
332    /// let issue = Issue::error("This is an error").with_annotation(Annotation::primary(span));
333    /// ```
334    #[must_use]
335    pub fn with_annotation(mut self, annotation: Annotation) -> Self {
336        self.annotations.push(annotation);
337
338        self
339    }
340
341    #[must_use]
342    pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
343        self.annotations.extend(annotation);
344
345        self
346    }
347
348    /// Add a note to this issue.
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// use mago_reporting::Issue;
354    ///
355    /// let issue = Issue::error("This is an error").with_note("This is a note");
356    /// ```
357    #[must_use]
358    pub fn with_note(mut self, note: impl Into<String>) -> Self {
359        self.notes.push(note.into());
360
361        self
362    }
363
364    /// Add a help message to this issue.
365    ///
366    /// This is useful for providing additional context to the user on how to resolve the issue.
367    ///
368    /// # Examples
369    ///
370    /// ```
371    /// use mago_reporting::Issue;
372    ///
373    /// let issue = Issue::error("This is an error").with_help("This is a help message");
374    /// ```
375    #[must_use]
376    pub fn with_help(mut self, help: impl Into<String>) -> Self {
377        self.help = Some(help.into());
378
379        self
380    }
381
382    /// Add a link to this issue.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use mago_reporting::Issue;
388    ///
389    /// let issue = Issue::error("This is an error").with_link("https://example.com");
390    /// ```
391    #[must_use]
392    pub fn with_link(mut self, link: impl Into<String>) -> Self {
393        self.link = Some(link.into());
394
395        self
396    }
397
398    /// Add a code modification suggestion to this issue.
399    #[must_use]
400    pub fn with_suggestion(mut self, source: SourceIdentifier, plan: FixPlan) -> Self {
401        self.suggestions.push((source, plan));
402
403        self
404    }
405
406    /// Take the code modification suggestion from this issue.
407    #[must_use]
408    pub fn take_suggestions(&mut self) -> Vec<(SourceIdentifier, FixPlan)> {
409        self.suggestions.drain(..).collect()
410    }
411}
412
413impl IssueCollection {
414    pub fn new() -> Self {
415        Self { issues: Vec::new() }
416    }
417
418    pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
419        Self { issues: issues.into_iter().collect() }
420    }
421
422    pub fn push(&mut self, issue: Issue) {
423        if self.issues.contains(&issue) {
424            return; // Avoid duplicates
425        }
426
427        self.issues.push(issue);
428    }
429
430    pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
431        self.issues.extend(issues);
432    }
433
434    pub fn shrink_to_fit(&mut self) {
435        self.issues.shrink_to_fit();
436    }
437
438    pub fn is_empty(&self) -> bool {
439        self.issues.is_empty()
440    }
441
442    pub fn len(&self) -> usize {
443        self.issues.len()
444    }
445
446    /// Filters the issues in the collection to only include those with a severity level
447    /// lower than or equal to the given level.
448    pub fn with_maximum_level(self, level: Level) -> Self {
449        Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
450    }
451
452    /// Filters the issues in the collection to only include those with a severity level
453    ///  higher than or equal to the given level.
454    pub fn with_minimum_level(self, level: Level) -> Self {
455        Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
456    }
457
458    /// Returns `true` if the collection contains any issues with a severity level
459    ///  higher than or equal to the given level.
460    pub fn has_minimum_level(&self, level: Level) -> bool {
461        self.issues.iter().any(|issue| issue.level >= level)
462    }
463
464    /// Returns the number of issues in the collection with the given severity level.
465    pub fn get_level_count(&self, level: Level) -> usize {
466        self.issues.iter().filter(|issue| issue.level == level).count()
467    }
468
469    /// Returns the highest severity level of the issues in the collection.
470    pub fn get_highest_level(&self) -> Option<Level> {
471        self.issues.iter().map(|issue| issue.level).max()
472    }
473
474    pub fn with_code(self, code: impl Into<String>) -> IssueCollection {
475        let code = code.into();
476
477        Self { issues: self.issues.into_iter().map(|issue| issue.with_code(&code)).collect() }
478    }
479
480    pub fn take_suggestions(&mut self) -> impl Iterator<Item = (SourceIdentifier, FixPlan)> + '_ {
481        self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
482    }
483
484    pub fn only_fixable(self) -> impl Iterator<Item = Issue> {
485        self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty())
486    }
487
488    /// Sorts the issues in the collection.
489    ///
490    /// The issues are sorted by severity level in descending order,
491    /// then by code in ascending order, and finally by the primary annotation span.
492    pub fn sorted(self) -> Self {
493        let mut issues = self.issues;
494
495        issues.sort_by(|a, b| match a.level.cmp(&b.level) {
496            Ordering::Greater => Ordering::Greater,
497            Ordering::Less => Ordering::Less,
498            Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
499                Ordering::Less => Ordering::Less,
500                Ordering::Greater => Ordering::Greater,
501                Ordering::Equal => {
502                    let a_span = a
503                        .annotations
504                        .iter()
505                        .find(|annotation| annotation.is_primary())
506                        .map(|annotation| annotation.span);
507
508                    let b_span = b
509                        .annotations
510                        .iter()
511                        .find(|annotation| annotation.is_primary())
512                        .map(|annotation| annotation.span);
513
514                    match (a_span, b_span) {
515                        (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
516                        (Some(_), None) => Ordering::Less,
517                        (None, Some(_)) => Ordering::Greater,
518                        (None, None) => Ordering::Equal,
519                    }
520                }
521            },
522        });
523
524        Self { issues }
525    }
526
527    pub fn iter(&self) -> impl Iterator<Item = &Issue> {
528        self.issues.iter()
529    }
530
531    pub fn to_fix_plans(self) -> HashMap<SourceIdentifier, FixPlan> {
532        let mut plans: HashMap<SourceIdentifier, FixPlan> = HashMap::default();
533        for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
534            for suggestion in issue.suggestions.into_iter() {
535                match plans.entry(suggestion.0) {
536                    Entry::Occupied(mut occupied_entry) => {
537                        occupied_entry.get_mut().merge(suggestion.1);
538                    }
539                    Entry::Vacant(vacant_entry) => {
540                        vacant_entry.insert(suggestion.1);
541                    }
542                }
543            }
544        }
545
546        plans
547    }
548}
549
550impl IntoIterator for IssueCollection {
551    type Item = Issue;
552
553    type IntoIter = std::vec::IntoIter<Issue>;
554
555    fn into_iter(self) -> Self::IntoIter {
556        self.issues.into_iter()
557    }
558}
559
560impl Default for IssueCollection {
561    fn default() -> Self {
562        Self::new()
563    }
564}
565
566impl IntoIterator for Issue {
567    type Item = Issue;
568    type IntoIter = Once<Issue>;
569
570    fn into_iter(self) -> Self::IntoIter {
571        std::iter::once(self)
572    }
573}
574
575impl FromIterator<Issue> for IssueCollection {
576    fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
577        Self { issues: iter.into_iter().collect() }
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    pub fn test_highest_collection_level() {
587        let mut collection = IssueCollection::from(vec![]);
588        assert_eq!(collection.get_highest_level(), None);
589
590        collection.push(Issue::note("note"));
591        assert_eq!(collection.get_highest_level(), Some(Level::Note));
592
593        collection.push(Issue::help("help"));
594        assert_eq!(collection.get_highest_level(), Some(Level::Help));
595
596        collection.push(Issue::warning("warning"));
597        assert_eq!(collection.get_highest_level(), Some(Level::Warning));
598
599        collection.push(Issue::error("error"));
600        assert_eq!(collection.get_highest_level(), Some(Level::Error));
601    }
602
603    #[test]
604    pub fn test_level_downgrade() {
605        assert_eq!(Level::Error.downgrade(), Level::Warning);
606        assert_eq!(Level::Warning.downgrade(), Level::Help);
607        assert_eq!(Level::Help.downgrade(), Level::Note);
608        assert_eq!(Level::Note.downgrade(), Level::Note);
609    }
610
611    #[test]
612    pub fn test_issue_collection_with_maximum_level() {
613        let mut collection = IssueCollection::from(vec![
614            Issue::error("error"),
615            Issue::warning("warning"),
616            Issue::help("help"),
617            Issue::note("note"),
618        ]);
619
620        collection = collection.with_maximum_level(Level::Warning);
621        assert_eq!(collection.len(), 3);
622        assert_eq!(
623            collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
624            vec![Level::Warning, Level::Help, Level::Note]
625        );
626    }
627
628    #[test]
629    pub fn test_issue_collection_with_minimum_level() {
630        let mut collection = IssueCollection::from(vec![
631            Issue::error("error"),
632            Issue::warning("warning"),
633            Issue::help("help"),
634            Issue::note("note"),
635        ]);
636
637        collection = collection.with_minimum_level(Level::Warning);
638        assert_eq!(collection.len(), 2);
639        assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
640    }
641
642    #[test]
643    pub fn test_issue_collection_has_minimum_level() {
644        let mut collection = IssueCollection::from(vec![]);
645
646        assert!(!collection.has_minimum_level(Level::Error));
647        assert!(!collection.has_minimum_level(Level::Warning));
648        assert!(!collection.has_minimum_level(Level::Help));
649        assert!(!collection.has_minimum_level(Level::Note));
650
651        collection.push(Issue::note("note"));
652
653        assert!(!collection.has_minimum_level(Level::Error));
654        assert!(!collection.has_minimum_level(Level::Warning));
655        assert!(!collection.has_minimum_level(Level::Help));
656        assert!(collection.has_minimum_level(Level::Note));
657
658        collection.push(Issue::help("help"));
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::warning("warning"));
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::error("error"));
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
680    #[test]
681    pub fn test_issue_collection_level_count() {
682        let mut collection = IssueCollection::from(vec![]);
683
684        assert_eq!(collection.get_level_count(Level::Error), 0);
685        assert_eq!(collection.get_level_count(Level::Warning), 0);
686        assert_eq!(collection.get_level_count(Level::Help), 0);
687        assert_eq!(collection.get_level_count(Level::Note), 0);
688
689        collection.push(Issue::error("error"));
690
691        assert_eq!(collection.get_level_count(Level::Error), 1);
692        assert_eq!(collection.get_level_count(Level::Warning), 0);
693        assert_eq!(collection.get_level_count(Level::Help), 0);
694        assert_eq!(collection.get_level_count(Level::Note), 0);
695
696        collection.push(Issue::warning("warning"));
697
698        assert_eq!(collection.get_level_count(Level::Error), 1);
699        assert_eq!(collection.get_level_count(Level::Warning), 1);
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::help("help"));
704
705        assert_eq!(collection.get_level_count(Level::Error), 1);
706        assert_eq!(collection.get_level_count(Level::Warning), 1);
707        assert_eq!(collection.get_level_count(Level::Help), 1);
708        assert_eq!(collection.get_level_count(Level::Note), 0);
709
710        collection.push(Issue::note("note"));
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), 1);
715        assert_eq!(collection.get_level_count(Level::Note), 1);
716    }
717}