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