mago_reporting/
lib.rs

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