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 if self.issues.contains(&issue) {
461 return; }
463
464 self.issues.push(issue);
465 }
466
467 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
468 self.issues.extend(issues);
469 }
470
471 pub fn shrink_to_fit(&mut self) {
472 self.issues.shrink_to_fit();
473 }
474
475 pub fn is_empty(&self) -> bool {
476 self.issues.is_empty()
477 }
478
479 pub fn len(&self) -> usize {
480 self.issues.len()
481 }
482
483 pub fn with_maximum_level(self, level: Level) -> Self {
486 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
487 }
488
489 pub fn with_minimum_level(self, level: Level) -> Self {
492 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
493 }
494
495 pub fn has_minimum_level(&self, level: Level) -> bool {
498 self.issues.iter().any(|issue| issue.level >= level)
499 }
500
501 pub fn get_level_count(&self, level: Level) -> usize {
503 self.issues.iter().filter(|issue| issue.level == level).count()
504 }
505
506 pub fn get_highest_level(&self) -> Option<Level> {
508 self.issues.iter().map(|issue| issue.level).max()
509 }
510
511 pub fn get_lowest_level(&self) -> Option<Level> {
513 self.issues.iter().map(|issue| issue.level).min()
514 }
515
516 pub fn filter_out_ignored(&mut self, ignore: &[String]) {
517 self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
518 }
519
520 pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
521 self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
522 }
523
524 pub fn take_suggestions(&mut self) -> impl Iterator<Item = (FileId, FixPlan)> + '_ {
525 self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
526 }
527
528 pub fn filter_fixable(self) -> Self {
529 Self { issues: self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()).collect() }
530 }
531
532 pub fn sorted(self) -> Self {
537 let mut issues = self.issues;
538
539 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
540 Ordering::Greater => Ordering::Greater,
541 Ordering::Less => Ordering::Less,
542 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
543 Ordering::Less => Ordering::Less,
544 Ordering::Greater => Ordering::Greater,
545 Ordering::Equal => {
546 let a_span = a
547 .annotations
548 .iter()
549 .find(|annotation| annotation.is_primary())
550 .map(|annotation| annotation.span);
551
552 let b_span = b
553 .annotations
554 .iter()
555 .find(|annotation| annotation.is_primary())
556 .map(|annotation| annotation.span);
557
558 match (a_span, b_span) {
559 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
560 (Some(_), None) => Ordering::Less,
561 (None, Some(_)) => Ordering::Greater,
562 (None, None) => Ordering::Equal,
563 }
564 }
565 },
566 });
567
568 Self { issues }
569 }
570
571 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
572 self.issues.iter()
573 }
574
575 pub fn to_fix_plans(self) -> HashMap<FileId, FixPlan> {
576 let mut plans: HashMap<FileId, FixPlan> = HashMap::default();
577 for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
578 for suggestion in issue.suggestions.into_iter() {
579 match plans.entry(suggestion.0) {
580 Entry::Occupied(mut occupied_entry) => {
581 occupied_entry.get_mut().merge(suggestion.1);
582 }
583 Entry::Vacant(vacant_entry) => {
584 vacant_entry.insert(suggestion.1);
585 }
586 }
587 }
588 }
589
590 plans
591 }
592}
593
594impl IntoIterator for IssueCollection {
595 type Item = Issue;
596
597 type IntoIter = std::vec::IntoIter<Issue>;
598
599 fn into_iter(self) -> Self::IntoIter {
600 self.issues.into_iter()
601 }
602}
603
604impl Default for IssueCollection {
605 fn default() -> Self {
606 Self::new()
607 }
608}
609
610impl IntoIterator for Issue {
611 type Item = Issue;
612 type IntoIter = Once<Issue>;
613
614 fn into_iter(self) -> Self::IntoIter {
615 std::iter::once(self)
616 }
617}
618
619impl FromIterator<Issue> for IssueCollection {
620 fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
621 Self { issues: iter.into_iter().collect() }
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 pub fn test_highest_collection_level() {
631 let mut collection = IssueCollection::from(vec![]);
632 assert_eq!(collection.get_highest_level(), None);
633
634 collection.push(Issue::note("note"));
635 assert_eq!(collection.get_highest_level(), Some(Level::Note));
636
637 collection.push(Issue::help("help"));
638 assert_eq!(collection.get_highest_level(), Some(Level::Help));
639
640 collection.push(Issue::warning("warning"));
641 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
642
643 collection.push(Issue::error("error"));
644 assert_eq!(collection.get_highest_level(), Some(Level::Error));
645 }
646
647 #[test]
648 pub fn test_level_downgrade() {
649 assert_eq!(Level::Error.downgrade(), Level::Warning);
650 assert_eq!(Level::Warning.downgrade(), Level::Help);
651 assert_eq!(Level::Help.downgrade(), Level::Note);
652 assert_eq!(Level::Note.downgrade(), Level::Note);
653 }
654
655 #[test]
656 pub fn test_issue_collection_with_maximum_level() {
657 let mut collection = IssueCollection::from(vec![
658 Issue::error("error"),
659 Issue::warning("warning"),
660 Issue::help("help"),
661 Issue::note("note"),
662 ]);
663
664 collection = collection.with_maximum_level(Level::Warning);
665 assert_eq!(collection.len(), 3);
666 assert_eq!(
667 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
668 vec![Level::Warning, Level::Help, Level::Note]
669 );
670 }
671
672 #[test]
673 pub fn test_issue_collection_with_minimum_level() {
674 let mut collection = IssueCollection::from(vec![
675 Issue::error("error"),
676 Issue::warning("warning"),
677 Issue::help("help"),
678 Issue::note("note"),
679 ]);
680
681 collection = collection.with_minimum_level(Level::Warning);
682 assert_eq!(collection.len(), 2);
683 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
684 }
685
686 #[test]
687 pub fn test_issue_collection_has_minimum_level() {
688 let mut collection = IssueCollection::from(vec![]);
689
690 assert!(!collection.has_minimum_level(Level::Error));
691 assert!(!collection.has_minimum_level(Level::Warning));
692 assert!(!collection.has_minimum_level(Level::Help));
693 assert!(!collection.has_minimum_level(Level::Note));
694
695 collection.push(Issue::note("note"));
696
697 assert!(!collection.has_minimum_level(Level::Error));
698 assert!(!collection.has_minimum_level(Level::Warning));
699 assert!(!collection.has_minimum_level(Level::Help));
700 assert!(collection.has_minimum_level(Level::Note));
701
702 collection.push(Issue::help("help"));
703
704 assert!(!collection.has_minimum_level(Level::Error));
705 assert!(!collection.has_minimum_level(Level::Warning));
706 assert!(collection.has_minimum_level(Level::Help));
707 assert!(collection.has_minimum_level(Level::Note));
708
709 collection.push(Issue::warning("warning"));
710
711 assert!(!collection.has_minimum_level(Level::Error));
712 assert!(collection.has_minimum_level(Level::Warning));
713 assert!(collection.has_minimum_level(Level::Help));
714 assert!(collection.has_minimum_level(Level::Note));
715
716 collection.push(Issue::error("error"));
717
718 assert!(collection.has_minimum_level(Level::Error));
719 assert!(collection.has_minimum_level(Level::Warning));
720 assert!(collection.has_minimum_level(Level::Help));
721 assert!(collection.has_minimum_level(Level::Note));
722 }
723
724 #[test]
725 pub fn test_issue_collection_level_count() {
726 let mut collection = IssueCollection::from(vec![]);
727
728 assert_eq!(collection.get_level_count(Level::Error), 0);
729 assert_eq!(collection.get_level_count(Level::Warning), 0);
730 assert_eq!(collection.get_level_count(Level::Help), 0);
731 assert_eq!(collection.get_level_count(Level::Note), 0);
732
733 collection.push(Issue::error("error"));
734
735 assert_eq!(collection.get_level_count(Level::Error), 1);
736 assert_eq!(collection.get_level_count(Level::Warning), 0);
737 assert_eq!(collection.get_level_count(Level::Help), 0);
738 assert_eq!(collection.get_level_count(Level::Note), 0);
739
740 collection.push(Issue::warning("warning"));
741
742 assert_eq!(collection.get_level_count(Level::Error), 1);
743 assert_eq!(collection.get_level_count(Level::Warning), 1);
744 assert_eq!(collection.get_level_count(Level::Help), 0);
745 assert_eq!(collection.get_level_count(Level::Note), 0);
746
747 collection.push(Issue::help("help"));
748
749 assert_eq!(collection.get_level_count(Level::Error), 1);
750 assert_eq!(collection.get_level_count(Level::Warning), 1);
751 assert_eq!(collection.get_level_count(Level::Help), 1);
752 assert_eq!(collection.get_level_count(Level::Note), 0);
753
754 collection.push(Issue::note("note"));
755
756 assert_eq!(collection.get_level_count(Level::Error), 1);
757 assert_eq!(collection.get_level_count(Level::Warning), 1);
758 assert_eq!(collection.get_level_count(Level::Help), 1);
759 assert_eq!(collection.get_level_count(Level::Note), 1);
760 }
761}