1use std::cmp::Ordering;
14use std::collections::hash_map::Entry;
15use std::iter::Once;
16use std::str::FromStr;
17
18use ahash::HashMap;
19use schemars::JsonSchema;
20use serde::Deserialize;
21use serde::Serialize;
22use strum::Display;
23use strum::VariantNames;
24
25use mago_database::file::FileId;
26use mago_fixer::FixPlan;
27use mago_span::Span;
28
29mod formatter;
30mod internal;
31
32pub mod baseline;
33pub mod color;
34pub mod error;
35pub mod output;
36pub mod reporter;
37
38pub use color::ColorChoice;
39pub use formatter::ReportingFormat;
40pub use output::ReportingTarget;
41
42#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
44pub enum AnnotationKind {
45 Primary,
47 Secondary,
49}
50
51#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
53pub struct Annotation {
54 pub message: Option<String>,
56 pub kind: AnnotationKind,
58 pub span: Span,
60}
61
62#[derive(
64 Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames, JsonSchema,
65)]
66#[strum(serialize_all = "lowercase")]
67pub enum Level {
68 #[serde(alias = "note")]
70 Note,
71 #[serde(alias = "help")]
73 Help,
74 #[serde(alias = "warning", alias = "warn")]
76 Warning,
77 #[serde(alias = "error", alias = "err")]
79 Error,
80}
81
82impl FromStr for Level {
83 type Err = ();
84
85 fn from_str(s: &str) -> Result<Self, Self::Err> {
86 match s.to_lowercase().as_str() {
87 "note" => Ok(Self::Note),
88 "help" => Ok(Self::Help),
89 "warning" => Ok(Self::Warning),
90 "error" => Ok(Self::Error),
91 _ => Err(()),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
98pub struct Issue {
99 pub level: Level,
101 pub code: Option<String>,
103 pub message: String,
105 pub notes: Vec<String>,
107 pub help: Option<String>,
109 pub link: Option<String>,
111 pub annotations: Vec<Annotation>,
113 pub suggestions: Vec<(FileId, FixPlan)>,
115}
116
117#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
119pub struct IssueCollection {
120 issues: Vec<Issue>,
121}
122
123impl AnnotationKind {
124 #[inline]
126 pub const fn is_primary(&self) -> bool {
127 matches!(self, AnnotationKind::Primary)
128 }
129
130 #[inline]
132 pub const fn is_secondary(&self) -> bool {
133 matches!(self, AnnotationKind::Secondary)
134 }
135}
136
137impl Annotation {
138 pub fn new(kind: AnnotationKind, span: Span) -> Self {
155 Self { message: None, kind, span }
156 }
157
158 pub fn primary(span: Span) -> Self {
175 Self::new(AnnotationKind::Primary, span)
176 }
177
178 pub fn secondary(span: Span) -> Self {
195 Self::new(AnnotationKind::Secondary, span)
196 }
197
198 #[must_use]
215 pub fn with_message(mut self, message: impl Into<String>) -> Self {
216 self.message = Some(message.into());
217
218 self
219 }
220
221 pub fn is_primary(&self) -> bool {
223 self.kind == AnnotationKind::Primary
224 }
225}
226
227impl Level {
228 pub fn downgrade(&self) -> Self {
255 match self {
256 Level::Error => Level::Warning,
257 Level::Warning => Level::Help,
258 Level::Help | Level::Note => Level::Note,
259 }
260 }
261}
262
263impl Issue {
264 pub fn new(level: Level, message: impl Into<String>) -> Self {
274 Self {
275 level,
276 code: None,
277 message: message.into(),
278 annotations: Vec::new(),
279 notes: Vec::new(),
280 help: None,
281 link: None,
282 suggestions: Vec::new(),
283 }
284 }
285
286 pub fn error(message: impl Into<String>) -> Self {
296 Self::new(Level::Error, message)
297 }
298
299 pub fn warning(message: impl Into<String>) -> Self {
309 Self::new(Level::Warning, message)
310 }
311
312 pub fn help(message: impl Into<String>) -> Self {
322 Self::new(Level::Help, message)
323 }
324
325 pub fn note(message: impl Into<String>) -> Self {
335 Self::new(Level::Note, message)
336 }
337
338 #[must_use]
348 pub fn with_code(mut self, code: impl Into<String>) -> Self {
349 self.code = Some(code.into());
350
351 self
352 }
353
354 #[must_use]
372 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
373 self.annotations.push(annotation);
374
375 self
376 }
377
378 #[must_use]
379 pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
380 self.annotations.extend(annotation);
381
382 self
383 }
384
385 #[must_use]
395 pub fn with_note(mut self, note: impl Into<String>) -> Self {
396 self.notes.push(note.into());
397
398 self
399 }
400
401 #[must_use]
413 pub fn with_help(mut self, help: impl Into<String>) -> Self {
414 self.help = Some(help.into());
415
416 self
417 }
418
419 #[must_use]
429 pub fn with_link(mut self, link: impl Into<String>) -> Self {
430 self.link = Some(link.into());
431
432 self
433 }
434
435 #[must_use]
437 pub fn with_suggestion(mut self, file_id: FileId, plan: FixPlan) -> Self {
438 self.suggestions.push((file_id, plan));
439
440 self
441 }
442
443 #[must_use]
445 pub fn take_suggestions(&mut self) -> Vec<(FileId, FixPlan)> {
446 self.suggestions.drain(..).collect()
447 }
448}
449
450impl IssueCollection {
451 pub fn new() -> Self {
452 Self { issues: Vec::new() }
453 }
454
455 pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
456 Self { issues: issues.into_iter().collect() }
457 }
458
459 pub fn push(&mut self, issue: Issue) {
460 self.issues.push(issue);
461 }
462
463 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
464 self.issues.extend(issues);
465 }
466
467 pub fn shrink_to_fit(&mut self) {
468 self.issues.shrink_to_fit();
469 }
470
471 pub fn is_empty(&self) -> bool {
472 self.issues.is_empty()
473 }
474
475 pub fn len(&self) -> usize {
476 self.issues.len()
477 }
478
479 pub fn with_maximum_level(self, level: Level) -> Self {
482 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
483 }
484
485 pub fn with_minimum_level(self, level: Level) -> Self {
488 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
489 }
490
491 pub fn has_minimum_level(&self, level: Level) -> bool {
494 self.issues.iter().any(|issue| issue.level >= level)
495 }
496
497 pub fn get_level_count(&self, level: Level) -> usize {
499 self.issues.iter().filter(|issue| issue.level == level).count()
500 }
501
502 pub fn get_highest_level(&self) -> Option<Level> {
504 self.issues.iter().map(|issue| issue.level).max()
505 }
506
507 pub fn get_lowest_level(&self) -> Option<Level> {
509 self.issues.iter().map(|issue| issue.level).min()
510 }
511
512 pub fn filter_out_ignored(&mut self, ignore: &[String]) {
513 self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
514 }
515
516 pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
517 self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
518 }
519
520 pub fn take_suggestions(&mut self) -> impl Iterator<Item = (FileId, FixPlan)> + '_ {
521 self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
522 }
523
524 pub fn filter_fixable(self) -> Self {
525 Self { issues: self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()).collect() }
526 }
527
528 pub fn sorted(self) -> Self {
533 let mut issues = self.issues;
534
535 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
536 Ordering::Greater => Ordering::Greater,
537 Ordering::Less => Ordering::Less,
538 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
539 Ordering::Less => Ordering::Less,
540 Ordering::Greater => Ordering::Greater,
541 Ordering::Equal => {
542 let a_span = a
543 .annotations
544 .iter()
545 .find(|annotation| annotation.is_primary())
546 .map(|annotation| annotation.span);
547
548 let b_span = b
549 .annotations
550 .iter()
551 .find(|annotation| annotation.is_primary())
552 .map(|annotation| annotation.span);
553
554 match (a_span, b_span) {
555 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
556 (Some(_), None) => Ordering::Less,
557 (None, Some(_)) => Ordering::Greater,
558 (None, None) => Ordering::Equal,
559 }
560 }
561 },
562 });
563
564 Self { issues }
565 }
566
567 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
568 self.issues.iter()
569 }
570
571 pub fn to_fix_plans(self) -> HashMap<FileId, FixPlan> {
572 let mut plans: HashMap<FileId, FixPlan> = HashMap::default();
573 for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
574 for suggestion in issue.suggestions.into_iter() {
575 match plans.entry(suggestion.0) {
576 Entry::Occupied(mut occupied_entry) => {
577 occupied_entry.get_mut().merge(suggestion.1);
578 }
579 Entry::Vacant(vacant_entry) => {
580 vacant_entry.insert(suggestion.1);
581 }
582 }
583 }
584 }
585
586 plans
587 }
588}
589
590impl IntoIterator for IssueCollection {
591 type Item = Issue;
592
593 type IntoIter = std::vec::IntoIter<Issue>;
594
595 fn into_iter(self) -> Self::IntoIter {
596 self.issues.into_iter()
597 }
598}
599
600impl Default for IssueCollection {
601 fn default() -> Self {
602 Self::new()
603 }
604}
605
606impl IntoIterator for Issue {
607 type Item = Issue;
608 type IntoIter = Once<Issue>;
609
610 fn into_iter(self) -> Self::IntoIter {
611 std::iter::once(self)
612 }
613}
614
615impl FromIterator<Issue> for IssueCollection {
616 fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
617 Self { issues: iter.into_iter().collect() }
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 pub fn test_highest_collection_level() {
627 let mut collection = IssueCollection::from(vec![]);
628 assert_eq!(collection.get_highest_level(), None);
629
630 collection.push(Issue::note("note"));
631 assert_eq!(collection.get_highest_level(), Some(Level::Note));
632
633 collection.push(Issue::help("help"));
634 assert_eq!(collection.get_highest_level(), Some(Level::Help));
635
636 collection.push(Issue::warning("warning"));
637 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
638
639 collection.push(Issue::error("error"));
640 assert_eq!(collection.get_highest_level(), Some(Level::Error));
641 }
642
643 #[test]
644 pub fn test_level_downgrade() {
645 assert_eq!(Level::Error.downgrade(), Level::Warning);
646 assert_eq!(Level::Warning.downgrade(), Level::Help);
647 assert_eq!(Level::Help.downgrade(), Level::Note);
648 assert_eq!(Level::Note.downgrade(), Level::Note);
649 }
650
651 #[test]
652 pub fn test_issue_collection_with_maximum_level() {
653 let mut collection = IssueCollection::from(vec![
654 Issue::error("error"),
655 Issue::warning("warning"),
656 Issue::help("help"),
657 Issue::note("note"),
658 ]);
659
660 collection = collection.with_maximum_level(Level::Warning);
661 assert_eq!(collection.len(), 3);
662 assert_eq!(
663 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
664 vec![Level::Warning, Level::Help, Level::Note]
665 );
666 }
667
668 #[test]
669 pub fn test_issue_collection_with_minimum_level() {
670 let mut collection = IssueCollection::from(vec![
671 Issue::error("error"),
672 Issue::warning("warning"),
673 Issue::help("help"),
674 Issue::note("note"),
675 ]);
676
677 collection = collection.with_minimum_level(Level::Warning);
678 assert_eq!(collection.len(), 2);
679 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
680 }
681
682 #[test]
683 pub fn test_issue_collection_has_minimum_level() {
684 let mut collection = IssueCollection::from(vec![]);
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 collection.push(Issue::note("note"));
692
693 assert!(!collection.has_minimum_level(Level::Error));
694 assert!(!collection.has_minimum_level(Level::Warning));
695 assert!(!collection.has_minimum_level(Level::Help));
696 assert!(collection.has_minimum_level(Level::Note));
697
698 collection.push(Issue::help("help"));
699
700 assert!(!collection.has_minimum_level(Level::Error));
701 assert!(!collection.has_minimum_level(Level::Warning));
702 assert!(collection.has_minimum_level(Level::Help));
703 assert!(collection.has_minimum_level(Level::Note));
704
705 collection.push(Issue::warning("warning"));
706
707 assert!(!collection.has_minimum_level(Level::Error));
708 assert!(collection.has_minimum_level(Level::Warning));
709 assert!(collection.has_minimum_level(Level::Help));
710 assert!(collection.has_minimum_level(Level::Note));
711
712 collection.push(Issue::error("error"));
713
714 assert!(collection.has_minimum_level(Level::Error));
715 assert!(collection.has_minimum_level(Level::Warning));
716 assert!(collection.has_minimum_level(Level::Help));
717 assert!(collection.has_minimum_level(Level::Note));
718 }
719
720 #[test]
721 pub fn test_issue_collection_level_count() {
722 let mut collection = IssueCollection::from(vec![]);
723
724 assert_eq!(collection.get_level_count(Level::Error), 0);
725 assert_eq!(collection.get_level_count(Level::Warning), 0);
726 assert_eq!(collection.get_level_count(Level::Help), 0);
727 assert_eq!(collection.get_level_count(Level::Note), 0);
728
729 collection.push(Issue::error("error"));
730
731 assert_eq!(collection.get_level_count(Level::Error), 1);
732 assert_eq!(collection.get_level_count(Level::Warning), 0);
733 assert_eq!(collection.get_level_count(Level::Help), 0);
734 assert_eq!(collection.get_level_count(Level::Note), 0);
735
736 collection.push(Issue::warning("warning"));
737
738 assert_eq!(collection.get_level_count(Level::Error), 1);
739 assert_eq!(collection.get_level_count(Level::Warning), 1);
740 assert_eq!(collection.get_level_count(Level::Help), 0);
741 assert_eq!(collection.get_level_count(Level::Note), 0);
742
743 collection.push(Issue::help("help"));
744
745 assert_eq!(collection.get_level_count(Level::Error), 1);
746 assert_eq!(collection.get_level_count(Level::Warning), 1);
747 assert_eq!(collection.get_level_count(Level::Help), 1);
748 assert_eq!(collection.get_level_count(Level::Note), 0);
749
750 collection.push(Issue::note("note"));
751
752 assert_eq!(collection.get_level_count(Level::Error), 1);
753 assert_eq!(collection.get_level_count(Level::Warning), 1);
754 assert_eq!(collection.get_level_count(Level::Help), 1);
755 assert_eq!(collection.get_level_count(Level::Note), 1);
756 }
757}