1use std::cmp::Ordering;
14use std::iter::Once;
15use std::str::FromStr;
16
17use ahash::HashMap;
18use ahash::HashMapExt;
19use schemars::JsonSchema;
20use serde::Deserialize;
21use serde::Serialize;
22use strum::Display;
23use strum::VariantNames;
24
25use mago_database::file::FileId;
26use mago_span::Span;
27use mago_text_edit::TextEdit;
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
96type IssueEdits = Vec<TextEdit>;
97type IssueEditBatches = Vec<(Option<String>, IssueEdits)>;
98
99#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
101pub struct Issue {
102 pub level: Level,
104 pub code: Option<String>,
106 pub message: String,
108 pub notes: Vec<String>,
110 pub help: Option<String>,
112 pub link: Option<String>,
114 pub annotations: Vec<Annotation>,
116 pub edits: HashMap<FileId, IssueEdits>,
118}
119
120#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
122pub struct IssueCollection {
123 issues: Vec<Issue>,
124}
125
126impl AnnotationKind {
127 #[inline]
129 #[must_use]
130 pub const fn is_primary(&self) -> bool {
131 matches!(self, AnnotationKind::Primary)
132 }
133
134 #[inline]
136 #[must_use]
137 pub const fn is_secondary(&self) -> bool {
138 matches!(self, AnnotationKind::Secondary)
139 }
140}
141
142impl Annotation {
143 #[must_use]
160 pub fn new(kind: AnnotationKind, span: Span) -> Self {
161 Self { message: None, kind, span }
162 }
163
164 #[must_use]
181 pub fn primary(span: Span) -> Self {
182 Self::new(AnnotationKind::Primary, span)
183 }
184
185 #[must_use]
202 pub fn secondary(span: Span) -> Self {
203 Self::new(AnnotationKind::Secondary, span)
204 }
205
206 #[must_use]
223 pub fn with_message(mut self, message: impl Into<String>) -> Self {
224 self.message = Some(message.into());
225
226 self
227 }
228
229 #[must_use]
231 pub fn is_primary(&self) -> bool {
232 self.kind == AnnotationKind::Primary
233 }
234}
235
236impl Level {
237 #[must_use]
264 pub fn downgrade(&self) -> Self {
265 match self {
266 Level::Error => Level::Warning,
267 Level::Warning => Level::Help,
268 Level::Help | Level::Note => Level::Note,
269 }
270 }
271}
272
273impl Issue {
274 pub fn new(level: Level, message: impl Into<String>) -> Self {
284 Self {
285 level,
286 code: None,
287 message: message.into(),
288 annotations: Vec::new(),
289 notes: Vec::new(),
290 help: None,
291 link: None,
292 edits: HashMap::default(),
293 }
294 }
295
296 pub fn error(message: impl Into<String>) -> Self {
306 Self::new(Level::Error, message)
307 }
308
309 pub fn warning(message: impl Into<String>) -> Self {
319 Self::new(Level::Warning, message)
320 }
321
322 pub fn help(message: impl Into<String>) -> Self {
332 Self::new(Level::Help, message)
333 }
334
335 pub fn note(message: impl Into<String>) -> Self {
345 Self::new(Level::Note, message)
346 }
347
348 #[must_use]
358 pub fn with_code(mut self, code: impl Into<String>) -> Self {
359 self.code = Some(code.into());
360
361 self
362 }
363
364 #[must_use]
382 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
383 self.annotations.push(annotation);
384
385 self
386 }
387
388 #[must_use]
389 pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
390 self.annotations.extend(annotation);
391
392 self
393 }
394
395 #[must_use]
405 pub fn with_note(mut self, note: impl Into<String>) -> Self {
406 self.notes.push(note.into());
407
408 self
409 }
410
411 #[must_use]
423 pub fn with_help(mut self, help: impl Into<String>) -> Self {
424 self.help = Some(help.into());
425
426 self
427 }
428
429 #[must_use]
439 pub fn with_link(mut self, link: impl Into<String>) -> Self {
440 self.link = Some(link.into());
441
442 self
443 }
444
445 #[must_use]
447 pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
448 self.edits.entry(file_id).or_default().push(edit);
449
450 self
451 }
452
453 #[must_use]
455 pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
456 if !edits.is_empty() {
457 self.edits.entry(file_id).or_default().extend(edits);
458 }
459
460 self
461 }
462
463 #[must_use]
465 pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
466 std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
467 }
468}
469
470impl IssueCollection {
471 #[must_use]
472 pub fn new() -> Self {
473 Self { issues: Vec::new() }
474 }
475
476 pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
477 Self { issues: issues.into_iter().collect() }
478 }
479
480 pub fn push(&mut self, issue: Issue) {
481 self.issues.push(issue);
482 }
483
484 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
485 self.issues.extend(issues);
486 }
487
488 pub fn shrink_to_fit(&mut self) {
489 self.issues.shrink_to_fit();
490 }
491
492 #[must_use]
493 pub fn is_empty(&self) -> bool {
494 self.issues.is_empty()
495 }
496
497 #[must_use]
498 pub fn len(&self) -> usize {
499 self.issues.len()
500 }
501
502 #[must_use]
505 pub fn with_maximum_level(self, level: Level) -> Self {
506 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
507 }
508
509 #[must_use]
512 pub fn with_minimum_level(self, level: Level) -> Self {
513 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
514 }
515
516 #[must_use]
519 pub fn has_minimum_level(&self, level: Level) -> bool {
520 self.issues.iter().any(|issue| issue.level >= level)
521 }
522
523 #[must_use]
525 pub fn get_level_count(&self, level: Level) -> usize {
526 self.issues.iter().filter(|issue| issue.level == level).count()
527 }
528
529 #[must_use]
531 pub fn get_highest_level(&self) -> Option<Level> {
532 self.issues.iter().map(|issue| issue.level).max()
533 }
534
535 #[must_use]
537 pub fn get_lowest_level(&self) -> Option<Level> {
538 self.issues.iter().map(|issue| issue.level).min()
539 }
540
541 pub fn filter_out_ignored(&mut self, ignore: &[String]) {
542 self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
543 }
544
545 pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
546 self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
547 }
548
549 pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
550 self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
551 }
552
553 #[must_use]
555 pub fn with_edits(self) -> Self {
556 Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
557 }
558
559 #[must_use]
564 pub fn sorted(self) -> Self {
565 let mut issues = self.issues;
566
567 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
568 Ordering::Greater => Ordering::Greater,
569 Ordering::Less => Ordering::Less,
570 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
571 Ordering::Less => Ordering::Less,
572 Ordering::Greater => Ordering::Greater,
573 Ordering::Equal => {
574 let a_span = a
575 .annotations
576 .iter()
577 .find(|annotation| annotation.is_primary())
578 .map(|annotation| annotation.span);
579
580 let b_span = b
581 .annotations
582 .iter()
583 .find(|annotation| annotation.is_primary())
584 .map(|annotation| annotation.span);
585
586 match (a_span, b_span) {
587 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
588 (Some(_), None) => Ordering::Less,
589 (None, Some(_)) => Ordering::Greater,
590 (None, None) => Ordering::Equal,
591 }
592 }
593 },
594 });
595
596 Self { issues }
597 }
598
599 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
600 self.issues.iter()
601 }
602
603 #[must_use]
611 pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
612 let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
613 for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
614 let code = issue.code;
615 for (file_id, edit_list) in issue.edits {
616 result.entry(file_id).or_default().push((code.clone(), edit_list));
617 }
618 }
619
620 result
621 }
622}
623
624impl IntoIterator for IssueCollection {
625 type Item = Issue;
626
627 type IntoIter = std::vec::IntoIter<Issue>;
628
629 fn into_iter(self) -> Self::IntoIter {
630 self.issues.into_iter()
631 }
632}
633
634impl<'a> IntoIterator for &'a IssueCollection {
635 type Item = &'a Issue;
636
637 type IntoIter = std::slice::Iter<'a, Issue>;
638
639 fn into_iter(self) -> Self::IntoIter {
640 self.issues.iter()
641 }
642}
643
644impl Default for IssueCollection {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650impl IntoIterator for Issue {
651 type Item = Issue;
652 type IntoIter = Once<Issue>;
653
654 fn into_iter(self) -> Self::IntoIter {
655 std::iter::once(self)
656 }
657}
658
659impl FromIterator<Issue> for IssueCollection {
660 fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
661 Self { issues: iter.into_iter().collect() }
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
670 pub fn test_highest_collection_level() {
671 let mut collection = IssueCollection::from(vec![]);
672 assert_eq!(collection.get_highest_level(), None);
673
674 collection.push(Issue::note("note"));
675 assert_eq!(collection.get_highest_level(), Some(Level::Note));
676
677 collection.push(Issue::help("help"));
678 assert_eq!(collection.get_highest_level(), Some(Level::Help));
679
680 collection.push(Issue::warning("warning"));
681 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
682
683 collection.push(Issue::error("error"));
684 assert_eq!(collection.get_highest_level(), Some(Level::Error));
685 }
686
687 #[test]
688 pub fn test_level_downgrade() {
689 assert_eq!(Level::Error.downgrade(), Level::Warning);
690 assert_eq!(Level::Warning.downgrade(), Level::Help);
691 assert_eq!(Level::Help.downgrade(), Level::Note);
692 assert_eq!(Level::Note.downgrade(), Level::Note);
693 }
694
695 #[test]
696 pub fn test_issue_collection_with_maximum_level() {
697 let mut collection = IssueCollection::from(vec![
698 Issue::error("error"),
699 Issue::warning("warning"),
700 Issue::help("help"),
701 Issue::note("note"),
702 ]);
703
704 collection = collection.with_maximum_level(Level::Warning);
705 assert_eq!(collection.len(), 3);
706 assert_eq!(
707 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
708 vec![Level::Warning, Level::Help, Level::Note]
709 );
710 }
711
712 #[test]
713 pub fn test_issue_collection_with_minimum_level() {
714 let mut collection = IssueCollection::from(vec![
715 Issue::error("error"),
716 Issue::warning("warning"),
717 Issue::help("help"),
718 Issue::note("note"),
719 ]);
720
721 collection = collection.with_minimum_level(Level::Warning);
722 assert_eq!(collection.len(), 2);
723 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
724 }
725
726 #[test]
727 pub fn test_issue_collection_has_minimum_level() {
728 let mut collection = IssueCollection::from(vec![]);
729
730 assert!(!collection.has_minimum_level(Level::Error));
731 assert!(!collection.has_minimum_level(Level::Warning));
732 assert!(!collection.has_minimum_level(Level::Help));
733 assert!(!collection.has_minimum_level(Level::Note));
734
735 collection.push(Issue::note("note"));
736
737 assert!(!collection.has_minimum_level(Level::Error));
738 assert!(!collection.has_minimum_level(Level::Warning));
739 assert!(!collection.has_minimum_level(Level::Help));
740 assert!(collection.has_minimum_level(Level::Note));
741
742 collection.push(Issue::help("help"));
743
744 assert!(!collection.has_minimum_level(Level::Error));
745 assert!(!collection.has_minimum_level(Level::Warning));
746 assert!(collection.has_minimum_level(Level::Help));
747 assert!(collection.has_minimum_level(Level::Note));
748
749 collection.push(Issue::warning("warning"));
750
751 assert!(!collection.has_minimum_level(Level::Error));
752 assert!(collection.has_minimum_level(Level::Warning));
753 assert!(collection.has_minimum_level(Level::Help));
754 assert!(collection.has_minimum_level(Level::Note));
755
756 collection.push(Issue::error("error"));
757
758 assert!(collection.has_minimum_level(Level::Error));
759 assert!(collection.has_minimum_level(Level::Warning));
760 assert!(collection.has_minimum_level(Level::Help));
761 assert!(collection.has_minimum_level(Level::Note));
762 }
763
764 #[test]
765 pub fn test_issue_collection_level_count() {
766 let mut collection = IssueCollection::from(vec![]);
767
768 assert_eq!(collection.get_level_count(Level::Error), 0);
769 assert_eq!(collection.get_level_count(Level::Warning), 0);
770 assert_eq!(collection.get_level_count(Level::Help), 0);
771 assert_eq!(collection.get_level_count(Level::Note), 0);
772
773 collection.push(Issue::error("error"));
774
775 assert_eq!(collection.get_level_count(Level::Error), 1);
776 assert_eq!(collection.get_level_count(Level::Warning), 0);
777 assert_eq!(collection.get_level_count(Level::Help), 0);
778 assert_eq!(collection.get_level_count(Level::Note), 0);
779
780 collection.push(Issue::warning("warning"));
781
782 assert_eq!(collection.get_level_count(Level::Error), 1);
783 assert_eq!(collection.get_level_count(Level::Warning), 1);
784 assert_eq!(collection.get_level_count(Level::Help), 0);
785 assert_eq!(collection.get_level_count(Level::Note), 0);
786
787 collection.push(Issue::help("help"));
788
789 assert_eq!(collection.get_level_count(Level::Error), 1);
790 assert_eq!(collection.get_level_count(Level::Warning), 1);
791 assert_eq!(collection.get_level_count(Level::Help), 1);
792 assert_eq!(collection.get_level_count(Level::Note), 0);
793
794 collection.push(Issue::note("note"));
795
796 assert_eq!(collection.get_level_count(Level::Error), 1);
797 assert_eq!(collection.get_level_count(Level::Warning), 1);
798 assert_eq!(collection.get_level_count(Level::Help), 1);
799 assert_eq!(collection.get_level_count(Level::Note), 1);
800 }
801}