1use std::cmp::Ordering;
14use std::iter::Once;
15use std::str::FromStr;
16
17use foldhash::HashMap;
18use foldhash::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
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
34#[serde(untagged)]
35pub enum IgnoreEntry {
36 Code(String),
38 Scoped {
40 code: String,
41 #[serde(rename = "in", deserialize_with = "one_or_many")]
42 paths: Vec<String>,
43 },
44}
45
46fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
47where
48 D: serde::Deserializer<'de>,
49{
50 #[derive(Deserialize)]
51 #[serde(untagged)]
52 enum OneOrMany {
53 One(String),
54 Many(Vec<String>),
55 }
56
57 match OneOrMany::deserialize(deserializer)? {
58 OneOrMany::One(s) => Ok(vec![s]),
59 OneOrMany::Many(v) => Ok(v),
60 }
61}
62
63mod formatter;
64mod internal;
65
66pub mod baseline;
67pub mod color;
68pub mod error;
69pub mod output;
70pub mod reporter;
71
72pub use color::ColorChoice;
73pub use formatter::ReportingFormat;
74pub use output::ReportingTarget;
75
76#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
78pub enum AnnotationKind {
79 Primary,
81 Secondary,
83}
84
85#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
87pub struct Annotation {
88 pub message: Option<String>,
90 pub kind: AnnotationKind,
92 pub span: Span,
94}
95
96#[derive(
98 Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames, JsonSchema,
99)]
100#[strum(serialize_all = "lowercase")]
101pub enum Level {
102 #[serde(alias = "note")]
104 Note,
105 #[serde(alias = "help")]
107 Help,
108 #[serde(alias = "warning", alias = "warn")]
110 Warning,
111 #[serde(alias = "error", alias = "err")]
113 Error,
114}
115
116impl FromStr for Level {
117 type Err = ();
118
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
120 match s.to_lowercase().as_str() {
121 "note" => Ok(Self::Note),
122 "help" => Ok(Self::Help),
123 "warning" => Ok(Self::Warning),
124 "error" => Ok(Self::Error),
125 _ => Err(()),
126 }
127 }
128}
129
130type IssueEdits = Vec<TextEdit>;
131type IssueEditBatches = Vec<(Option<String>, IssueEdits)>;
132
133#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
135pub struct Issue {
136 pub level: Level,
138 pub code: Option<String>,
140 pub message: String,
142 pub notes: Vec<String>,
144 pub help: Option<String>,
146 pub link: Option<String>,
148 pub annotations: Vec<Annotation>,
150 pub edits: HashMap<FileId, IssueEdits>,
152}
153
154#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
156pub struct IssueCollection {
157 issues: Vec<Issue>,
158}
159
160impl AnnotationKind {
161 #[inline]
163 #[must_use]
164 pub const fn is_primary(&self) -> bool {
165 matches!(self, AnnotationKind::Primary)
166 }
167
168 #[inline]
170 #[must_use]
171 pub const fn is_secondary(&self) -> bool {
172 matches!(self, AnnotationKind::Secondary)
173 }
174}
175
176impl Annotation {
177 #[must_use]
194 pub fn new(kind: AnnotationKind, span: Span) -> Self {
195 Self { message: None, kind, span }
196 }
197
198 #[must_use]
215 pub fn primary(span: Span) -> Self {
216 Self::new(AnnotationKind::Primary, span)
217 }
218
219 #[must_use]
236 pub fn secondary(span: Span) -> Self {
237 Self::new(AnnotationKind::Secondary, span)
238 }
239
240 #[must_use]
257 pub fn with_message(mut self, message: impl Into<String>) -> Self {
258 self.message = Some(message.into());
259
260 self
261 }
262
263 #[must_use]
265 pub fn is_primary(&self) -> bool {
266 self.kind == AnnotationKind::Primary
267 }
268}
269
270impl Level {
271 #[must_use]
298 pub fn downgrade(&self) -> Self {
299 match self {
300 Level::Error => Level::Warning,
301 Level::Warning => Level::Help,
302 Level::Help | Level::Note => Level::Note,
303 }
304 }
305}
306
307impl Issue {
308 pub fn new(level: Level, message: impl Into<String>) -> Self {
318 Self {
319 level,
320 code: None,
321 message: message.into(),
322 annotations: Vec::new(),
323 notes: Vec::new(),
324 help: None,
325 link: None,
326 edits: HashMap::default(),
327 }
328 }
329
330 pub fn error(message: impl Into<String>) -> Self {
340 Self::new(Level::Error, message)
341 }
342
343 pub fn warning(message: impl Into<String>) -> Self {
353 Self::new(Level::Warning, message)
354 }
355
356 pub fn help(message: impl Into<String>) -> Self {
366 Self::new(Level::Help, message)
367 }
368
369 pub fn note(message: impl Into<String>) -> Self {
379 Self::new(Level::Note, message)
380 }
381
382 #[must_use]
392 pub fn with_code(mut self, code: impl Into<String>) -> Self {
393 self.code = Some(code.into());
394
395 self
396 }
397
398 #[must_use]
416 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
417 self.annotations.push(annotation);
418
419 self
420 }
421
422 #[must_use]
423 pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
424 self.annotations.extend(annotation);
425
426 self
427 }
428
429 #[must_use]
433 pub fn primary_annotation(&self) -> Option<&Annotation> {
434 self.annotations.iter().filter(|annotation| annotation.is_primary()).min_by_key(|annotation| annotation.span)
435 }
436
437 #[must_use]
439 pub fn primary_span(&self) -> Option<Span> {
440 self.primary_annotation().map(|annotation| annotation.span)
441 }
442
443 #[must_use]
453 pub fn with_note(mut self, note: impl Into<String>) -> Self {
454 self.notes.push(note.into());
455
456 self
457 }
458
459 #[must_use]
471 pub fn with_help(mut self, help: impl Into<String>) -> Self {
472 self.help = Some(help.into());
473
474 self
475 }
476
477 #[must_use]
487 pub fn with_link(mut self, link: impl Into<String>) -> Self {
488 self.link = Some(link.into());
489
490 self
491 }
492
493 #[must_use]
495 pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
496 self.edits.entry(file_id).or_default().push(edit);
497
498 self
499 }
500
501 #[must_use]
503 pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
504 if !edits.is_empty() {
505 self.edits.entry(file_id).or_default().extend(edits);
506 }
507
508 self
509 }
510
511 #[must_use]
513 pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
514 std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
515 }
516}
517
518impl IssueCollection {
519 #[must_use]
520 pub fn new() -> Self {
521 Self { issues: Vec::new() }
522 }
523
524 pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
525 Self { issues: issues.into_iter().collect() }
526 }
527
528 pub fn push(&mut self, issue: Issue) {
529 self.issues.push(issue);
530 }
531
532 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
533 self.issues.extend(issues);
534 }
535
536 pub fn reserve(&mut self, additional: usize) {
537 self.issues.reserve(additional);
538 }
539
540 pub fn shrink_to_fit(&mut self) {
541 self.issues.shrink_to_fit();
542 }
543
544 #[must_use]
545 pub fn is_empty(&self) -> bool {
546 self.issues.is_empty()
547 }
548
549 #[must_use]
550 pub fn len(&self) -> usize {
551 self.issues.len()
552 }
553
554 #[must_use]
557 pub fn with_maximum_level(self, level: Level) -> Self {
558 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
559 }
560
561 #[must_use]
564 pub fn with_minimum_level(self, level: Level) -> Self {
565 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
566 }
567
568 #[must_use]
571 pub fn has_minimum_level(&self, level: Level) -> bool {
572 self.issues.iter().any(|issue| issue.level >= level)
573 }
574
575 #[must_use]
577 pub fn get_level_count(&self, level: Level) -> usize {
578 self.issues.iter().filter(|issue| issue.level == level).count()
579 }
580
581 #[must_use]
583 pub fn get_highest_level(&self) -> Option<Level> {
584 self.issues.iter().map(|issue| issue.level).max()
585 }
586
587 #[must_use]
589 pub fn get_lowest_level(&self) -> Option<Level> {
590 self.issues.iter().map(|issue| issue.level).min()
591 }
592
593 pub fn filter_out_ignored<F>(&mut self, ignore: &[IgnoreEntry], resolve_file_name: F)
594 where
595 F: Fn(FileId) -> Option<String>,
596 {
597 if ignore.is_empty() {
598 return;
599 }
600
601 self.issues.retain(|issue| {
602 let Some(code) = &issue.code else {
603 return true;
604 };
605
606 for entry in ignore {
607 match entry {
608 IgnoreEntry::Code(ignored) if ignored == code => return false,
609 IgnoreEntry::Scoped { code: ignored, paths } if ignored == code => {
610 let file_name = issue.primary_span().and_then(|span| resolve_file_name(span.file_id));
611
612 if let Some(name) = file_name
613 && is_path_match(&name, paths)
614 {
615 return false;
616 }
617 }
618 _ => {}
619 }
620 }
621
622 true
623 });
624 }
625
626 pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
627 self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
628 }
629
630 pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
631 self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
632 }
633
634 #[must_use]
636 pub fn with_edits(self) -> Self {
637 Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
638 }
639
640 #[must_use]
645 pub fn sorted(self) -> Self {
646 let mut issues = self.issues;
647
648 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
649 Ordering::Greater => Ordering::Greater,
650 Ordering::Less => Ordering::Less,
651 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
652 Ordering::Less => Ordering::Less,
653 Ordering::Greater => Ordering::Greater,
654 Ordering::Equal => {
655 let a_span = a.primary_span();
656 let b_span = b.primary_span();
657
658 match (a_span, b_span) {
659 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
660 (Some(_), None) => Ordering::Less,
661 (None, Some(_)) => Ordering::Greater,
662 (None, None) => Ordering::Equal,
663 }
664 }
665 },
666 });
667
668 Self { issues }
669 }
670
671 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
672 self.issues.iter()
673 }
674
675 #[must_use]
683 pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
684 let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
685 for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
686 let code = issue.code;
687 for (file_id, edit_list) in issue.edits {
688 result.entry(file_id).or_default().push((code.clone(), edit_list));
689 }
690 }
691
692 result
693 }
694}
695
696impl IntoIterator for IssueCollection {
697 type Item = Issue;
698
699 type IntoIter = std::vec::IntoIter<Issue>;
700
701 fn into_iter(self) -> Self::IntoIter {
702 self.issues.into_iter()
703 }
704}
705
706impl<'a> IntoIterator for &'a IssueCollection {
707 type Item = &'a Issue;
708
709 type IntoIter = std::slice::Iter<'a, Issue>;
710
711 fn into_iter(self) -> Self::IntoIter {
712 self.issues.iter()
713 }
714}
715
716impl Default for IssueCollection {
717 fn default() -> Self {
718 Self::new()
719 }
720}
721
722impl IntoIterator for Issue {
723 type Item = Issue;
724 type IntoIter = Once<Issue>;
725
726 fn into_iter(self) -> Self::IntoIter {
727 std::iter::once(self)
728 }
729}
730
731impl FromIterator<Issue> for IssueCollection {
732 fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
733 Self { issues: iter.into_iter().collect() }
734 }
735}
736
737fn is_path_match(file_name: &str, patterns: &[String]) -> bool {
738 patterns.iter().any(|pattern| {
739 if pattern.ends_with('/') {
740 file_name.starts_with(pattern.as_str())
741 } else {
742 let dir_prefix = format!("{pattern}/");
743 file_name.starts_with(&dir_prefix) || file_name == pattern
744 }
745 })
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 #[test]
753 pub fn test_highest_collection_level() {
754 let mut collection = IssueCollection::from(vec![]);
755 assert_eq!(collection.get_highest_level(), None);
756
757 collection.push(Issue::note("note"));
758 assert_eq!(collection.get_highest_level(), Some(Level::Note));
759
760 collection.push(Issue::help("help"));
761 assert_eq!(collection.get_highest_level(), Some(Level::Help));
762
763 collection.push(Issue::warning("warning"));
764 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
765
766 collection.push(Issue::error("error"));
767 assert_eq!(collection.get_highest_level(), Some(Level::Error));
768 }
769
770 #[test]
771 pub fn test_level_downgrade() {
772 assert_eq!(Level::Error.downgrade(), Level::Warning);
773 assert_eq!(Level::Warning.downgrade(), Level::Help);
774 assert_eq!(Level::Help.downgrade(), Level::Note);
775 assert_eq!(Level::Note.downgrade(), Level::Note);
776 }
777
778 #[test]
779 pub fn test_issue_collection_with_maximum_level() {
780 let mut collection = IssueCollection::from(vec![
781 Issue::error("error"),
782 Issue::warning("warning"),
783 Issue::help("help"),
784 Issue::note("note"),
785 ]);
786
787 collection = collection.with_maximum_level(Level::Warning);
788 assert_eq!(collection.len(), 3);
789 assert_eq!(
790 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
791 vec![Level::Warning, Level::Help, Level::Note]
792 );
793 }
794
795 #[test]
796 pub fn test_issue_collection_with_minimum_level() {
797 let mut collection = IssueCollection::from(vec![
798 Issue::error("error"),
799 Issue::warning("warning"),
800 Issue::help("help"),
801 Issue::note("note"),
802 ]);
803
804 collection = collection.with_minimum_level(Level::Warning);
805 assert_eq!(collection.len(), 2);
806 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
807 }
808
809 #[test]
810 pub fn test_issue_collection_has_minimum_level() {
811 let mut collection = IssueCollection::from(vec![]);
812
813 assert!(!collection.has_minimum_level(Level::Error));
814 assert!(!collection.has_minimum_level(Level::Warning));
815 assert!(!collection.has_minimum_level(Level::Help));
816 assert!(!collection.has_minimum_level(Level::Note));
817
818 collection.push(Issue::note("note"));
819
820 assert!(!collection.has_minimum_level(Level::Error));
821 assert!(!collection.has_minimum_level(Level::Warning));
822 assert!(!collection.has_minimum_level(Level::Help));
823 assert!(collection.has_minimum_level(Level::Note));
824
825 collection.push(Issue::help("help"));
826
827 assert!(!collection.has_minimum_level(Level::Error));
828 assert!(!collection.has_minimum_level(Level::Warning));
829 assert!(collection.has_minimum_level(Level::Help));
830 assert!(collection.has_minimum_level(Level::Note));
831
832 collection.push(Issue::warning("warning"));
833
834 assert!(!collection.has_minimum_level(Level::Error));
835 assert!(collection.has_minimum_level(Level::Warning));
836 assert!(collection.has_minimum_level(Level::Help));
837 assert!(collection.has_minimum_level(Level::Note));
838
839 collection.push(Issue::error("error"));
840
841 assert!(collection.has_minimum_level(Level::Error));
842 assert!(collection.has_minimum_level(Level::Warning));
843 assert!(collection.has_minimum_level(Level::Help));
844 assert!(collection.has_minimum_level(Level::Note));
845 }
846
847 #[test]
848 pub fn test_issue_collection_level_count() {
849 let mut collection = IssueCollection::from(vec![]);
850
851 assert_eq!(collection.get_level_count(Level::Error), 0);
852 assert_eq!(collection.get_level_count(Level::Warning), 0);
853 assert_eq!(collection.get_level_count(Level::Help), 0);
854 assert_eq!(collection.get_level_count(Level::Note), 0);
855
856 collection.push(Issue::error("error"));
857
858 assert_eq!(collection.get_level_count(Level::Error), 1);
859 assert_eq!(collection.get_level_count(Level::Warning), 0);
860 assert_eq!(collection.get_level_count(Level::Help), 0);
861 assert_eq!(collection.get_level_count(Level::Note), 0);
862
863 collection.push(Issue::warning("warning"));
864
865 assert_eq!(collection.get_level_count(Level::Error), 1);
866 assert_eq!(collection.get_level_count(Level::Warning), 1);
867 assert_eq!(collection.get_level_count(Level::Help), 0);
868 assert_eq!(collection.get_level_count(Level::Note), 0);
869
870 collection.push(Issue::help("help"));
871
872 assert_eq!(collection.get_level_count(Level::Error), 1);
873 assert_eq!(collection.get_level_count(Level::Warning), 1);
874 assert_eq!(collection.get_level_count(Level::Help), 1);
875 assert_eq!(collection.get_level_count(Level::Note), 0);
876
877 collection.push(Issue::note("note"));
878
879 assert_eq!(collection.get_level_count(Level::Error), 1);
880 assert_eq!(collection.get_level_count(Level::Warning), 1);
881 assert_eq!(collection.get_level_count(Level::Help), 1);
882 assert_eq!(collection.get_level_count(Level::Note), 1);
883 }
884
885 #[test]
886 pub fn test_primary_span_is_deterministic() {
887 let file = FileId::zero();
888 let span_later = Span::new(file, 20_u32.into(), 25_u32.into());
889 let span_earlier = Span::new(file, 5_u32.into(), 10_u32.into());
890
891 let issue = Issue::error("x")
892 .with_annotation(Annotation::primary(span_later))
893 .with_annotation(Annotation::primary(span_earlier));
894
895 assert_eq!(issue.primary_span(), Some(span_earlier));
896 }
897}