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