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