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]
439 pub fn with_note(mut self, note: impl Into<String>) -> Self {
440 self.notes.push(note.into());
441
442 self
443 }
444
445 #[must_use]
457 pub fn with_help(mut self, help: impl Into<String>) -> Self {
458 self.help = Some(help.into());
459
460 self
461 }
462
463 #[must_use]
473 pub fn with_link(mut self, link: impl Into<String>) -> Self {
474 self.link = Some(link.into());
475
476 self
477 }
478
479 #[must_use]
481 pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
482 self.edits.entry(file_id).or_default().push(edit);
483
484 self
485 }
486
487 #[must_use]
489 pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
490 if !edits.is_empty() {
491 self.edits.entry(file_id).or_default().extend(edits);
492 }
493
494 self
495 }
496
497 #[must_use]
499 pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
500 std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
501 }
502}
503
504impl IssueCollection {
505 #[must_use]
506 pub fn new() -> Self {
507 Self { issues: Vec::new() }
508 }
509
510 pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
511 Self { issues: issues.into_iter().collect() }
512 }
513
514 pub fn push(&mut self, issue: Issue) {
515 self.issues.push(issue);
516 }
517
518 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
519 self.issues.extend(issues);
520 }
521
522 pub fn reserve(&mut self, additional: usize) {
523 self.issues.reserve(additional);
524 }
525
526 pub fn shrink_to_fit(&mut self) {
527 self.issues.shrink_to_fit();
528 }
529
530 #[must_use]
531 pub fn is_empty(&self) -> bool {
532 self.issues.is_empty()
533 }
534
535 #[must_use]
536 pub fn len(&self) -> usize {
537 self.issues.len()
538 }
539
540 #[must_use]
543 pub fn with_maximum_level(self, level: Level) -> Self {
544 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
545 }
546
547 #[must_use]
550 pub fn with_minimum_level(self, level: Level) -> Self {
551 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
552 }
553
554 #[must_use]
557 pub fn has_minimum_level(&self, level: Level) -> bool {
558 self.issues.iter().any(|issue| issue.level >= level)
559 }
560
561 #[must_use]
563 pub fn get_level_count(&self, level: Level) -> usize {
564 self.issues.iter().filter(|issue| issue.level == level).count()
565 }
566
567 #[must_use]
569 pub fn get_highest_level(&self) -> Option<Level> {
570 self.issues.iter().map(|issue| issue.level).max()
571 }
572
573 #[must_use]
575 pub fn get_lowest_level(&self) -> Option<Level> {
576 self.issues.iter().map(|issue| issue.level).min()
577 }
578
579 pub fn filter_out_ignored<F>(&mut self, ignore: &[IgnoreEntry], resolve_file_name: F)
580 where
581 F: Fn(FileId) -> Option<String>,
582 {
583 if ignore.is_empty() {
584 return;
585 }
586
587 self.issues.retain(|issue| {
588 let Some(code) = &issue.code else {
589 return true;
590 };
591
592 for entry in ignore {
593 match entry {
594 IgnoreEntry::Code(ignored) if ignored == code => return false,
595 IgnoreEntry::Scoped { code: ignored, paths } if ignored == code => {
596 let file_name = issue
597 .annotations
598 .iter()
599 .find(|a| a.is_primary())
600 .and_then(|a| resolve_file_name(a.span.file_id));
601
602 if let Some(name) = file_name
603 && is_path_match(&name, paths)
604 {
605 return false;
606 }
607 }
608 _ => {}
609 }
610 }
611
612 true
613 });
614 }
615
616 pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
617 self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
618 }
619
620 pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
621 self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
622 }
623
624 #[must_use]
626 pub fn with_edits(self) -> Self {
627 Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
628 }
629
630 #[must_use]
635 pub fn sorted(self) -> Self {
636 let mut issues = self.issues;
637
638 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
639 Ordering::Greater => Ordering::Greater,
640 Ordering::Less => Ordering::Less,
641 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
642 Ordering::Less => Ordering::Less,
643 Ordering::Greater => Ordering::Greater,
644 Ordering::Equal => {
645 let a_span = a
646 .annotations
647 .iter()
648 .find(|annotation| annotation.is_primary())
649 .map(|annotation| annotation.span);
650
651 let b_span = b
652 .annotations
653 .iter()
654 .find(|annotation| annotation.is_primary())
655 .map(|annotation| annotation.span);
656
657 match (a_span, b_span) {
658 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
659 (Some(_), None) => Ordering::Less,
660 (None, Some(_)) => Ordering::Greater,
661 (None, None) => Ordering::Equal,
662 }
663 }
664 },
665 });
666
667 Self { issues }
668 }
669
670 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
671 self.issues.iter()
672 }
673
674 #[must_use]
682 pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
683 let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
684 for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
685 let code = issue.code;
686 for (file_id, edit_list) in issue.edits {
687 result.entry(file_id).or_default().push((code.clone(), edit_list));
688 }
689 }
690
691 result
692 }
693}
694
695impl IntoIterator for IssueCollection {
696 type Item = Issue;
697
698 type IntoIter = std::vec::IntoIter<Issue>;
699
700 fn into_iter(self) -> Self::IntoIter {
701 self.issues.into_iter()
702 }
703}
704
705impl<'a> IntoIterator for &'a IssueCollection {
706 type Item = &'a Issue;
707
708 type IntoIter = std::slice::Iter<'a, Issue>;
709
710 fn into_iter(self) -> Self::IntoIter {
711 self.issues.iter()
712 }
713}
714
715impl Default for IssueCollection {
716 fn default() -> Self {
717 Self::new()
718 }
719}
720
721impl IntoIterator for Issue {
722 type Item = Issue;
723 type IntoIter = Once<Issue>;
724
725 fn into_iter(self) -> Self::IntoIter {
726 std::iter::once(self)
727 }
728}
729
730impl FromIterator<Issue> for IssueCollection {
731 fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
732 Self { issues: iter.into_iter().collect() }
733 }
734}
735
736fn is_path_match(file_name: &str, patterns: &[String]) -> bool {
737 patterns.iter().any(|pattern| {
738 if pattern.ends_with('/') {
739 file_name.starts_with(pattern.as_str())
740 } else {
741 let dir_prefix = format!("{pattern}/");
742 file_name.starts_with(&dir_prefix) || file_name == pattern
743 }
744 })
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 #[test]
752 pub fn test_highest_collection_level() {
753 let mut collection = IssueCollection::from(vec![]);
754 assert_eq!(collection.get_highest_level(), None);
755
756 collection.push(Issue::note("note"));
757 assert_eq!(collection.get_highest_level(), Some(Level::Note));
758
759 collection.push(Issue::help("help"));
760 assert_eq!(collection.get_highest_level(), Some(Level::Help));
761
762 collection.push(Issue::warning("warning"));
763 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
764
765 collection.push(Issue::error("error"));
766 assert_eq!(collection.get_highest_level(), Some(Level::Error));
767 }
768
769 #[test]
770 pub fn test_level_downgrade() {
771 assert_eq!(Level::Error.downgrade(), Level::Warning);
772 assert_eq!(Level::Warning.downgrade(), Level::Help);
773 assert_eq!(Level::Help.downgrade(), Level::Note);
774 assert_eq!(Level::Note.downgrade(), Level::Note);
775 }
776
777 #[test]
778 pub fn test_issue_collection_with_maximum_level() {
779 let mut collection = IssueCollection::from(vec![
780 Issue::error("error"),
781 Issue::warning("warning"),
782 Issue::help("help"),
783 Issue::note("note"),
784 ]);
785
786 collection = collection.with_maximum_level(Level::Warning);
787 assert_eq!(collection.len(), 3);
788 assert_eq!(
789 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
790 vec![Level::Warning, Level::Help, Level::Note]
791 );
792 }
793
794 #[test]
795 pub fn test_issue_collection_with_minimum_level() {
796 let mut collection = IssueCollection::from(vec![
797 Issue::error("error"),
798 Issue::warning("warning"),
799 Issue::help("help"),
800 Issue::note("note"),
801 ]);
802
803 collection = collection.with_minimum_level(Level::Warning);
804 assert_eq!(collection.len(), 2);
805 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
806 }
807
808 #[test]
809 pub fn test_issue_collection_has_minimum_level() {
810 let mut collection = IssueCollection::from(vec![]);
811
812 assert!(!collection.has_minimum_level(Level::Error));
813 assert!(!collection.has_minimum_level(Level::Warning));
814 assert!(!collection.has_minimum_level(Level::Help));
815 assert!(!collection.has_minimum_level(Level::Note));
816
817 collection.push(Issue::note("note"));
818
819 assert!(!collection.has_minimum_level(Level::Error));
820 assert!(!collection.has_minimum_level(Level::Warning));
821 assert!(!collection.has_minimum_level(Level::Help));
822 assert!(collection.has_minimum_level(Level::Note));
823
824 collection.push(Issue::help("help"));
825
826 assert!(!collection.has_minimum_level(Level::Error));
827 assert!(!collection.has_minimum_level(Level::Warning));
828 assert!(collection.has_minimum_level(Level::Help));
829 assert!(collection.has_minimum_level(Level::Note));
830
831 collection.push(Issue::warning("warning"));
832
833 assert!(!collection.has_minimum_level(Level::Error));
834 assert!(collection.has_minimum_level(Level::Warning));
835 assert!(collection.has_minimum_level(Level::Help));
836 assert!(collection.has_minimum_level(Level::Note));
837
838 collection.push(Issue::error("error"));
839
840 assert!(collection.has_minimum_level(Level::Error));
841 assert!(collection.has_minimum_level(Level::Warning));
842 assert!(collection.has_minimum_level(Level::Help));
843 assert!(collection.has_minimum_level(Level::Note));
844 }
845
846 #[test]
847 pub fn test_issue_collection_level_count() {
848 let mut collection = IssueCollection::from(vec![]);
849
850 assert_eq!(collection.get_level_count(Level::Error), 0);
851 assert_eq!(collection.get_level_count(Level::Warning), 0);
852 assert_eq!(collection.get_level_count(Level::Help), 0);
853 assert_eq!(collection.get_level_count(Level::Note), 0);
854
855 collection.push(Issue::error("error"));
856
857 assert_eq!(collection.get_level_count(Level::Error), 1);
858 assert_eq!(collection.get_level_count(Level::Warning), 0);
859 assert_eq!(collection.get_level_count(Level::Help), 0);
860 assert_eq!(collection.get_level_count(Level::Note), 0);
861
862 collection.push(Issue::warning("warning"));
863
864 assert_eq!(collection.get_level_count(Level::Error), 1);
865 assert_eq!(collection.get_level_count(Level::Warning), 1);
866 assert_eq!(collection.get_level_count(Level::Help), 0);
867 assert_eq!(collection.get_level_count(Level::Note), 0);
868
869 collection.push(Issue::help("help"));
870
871 assert_eq!(collection.get_level_count(Level::Error), 1);
872 assert_eq!(collection.get_level_count(Level::Warning), 1);
873 assert_eq!(collection.get_level_count(Level::Help), 1);
874 assert_eq!(collection.get_level_count(Level::Note), 0);
875
876 collection.push(Issue::note("note"));
877
878 assert_eq!(collection.get_level_count(Level::Error), 1);
879 assert_eq!(collection.get_level_count(Level::Warning), 1);
880 assert_eq!(collection.get_level_count(Level::Help), 1);
881 assert_eq!(collection.get_level_count(Level::Note), 1);
882 }
883}