1use std::cmp::Ordering;
2use std::collections::hash_map::Entry;
3use std::iter::Once;
4use std::str::FromStr;
5
6use ahash::HashMap;
7use serde::Deserialize;
8use serde::Serialize;
9use strum::Display;
10use strum::VariantNames;
11
12use mago_database::file::FileId;
13use mago_fixer::FixPlan;
14use mago_span::Span;
15
16mod internal;
17
18pub mod error;
19pub mod reporter;
20
21#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
23pub enum AnnotationKind {
24 Primary,
26 Secondary,
28}
29
30#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
32pub struct Annotation {
33 pub message: Option<String>,
35 pub kind: AnnotationKind,
37 pub span: Span,
39}
40
41#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames)]
43#[strum(serialize_all = "lowercase")]
44pub enum Level {
45 #[serde(alias = "note")]
47 Note,
48 #[serde(alias = "help")]
50 Help,
51 #[serde(alias = "warning", alias = "warn")]
53 Warning,
54 #[serde(alias = "error", alias = "err")]
56 Error,
57}
58
59impl FromStr for Level {
60 type Err = ();
61
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 match s.to_lowercase().as_str() {
64 "note" => Ok(Self::Note),
65 "help" => Ok(Self::Help),
66 "warning" => Ok(Self::Warning),
67 "error" => Ok(Self::Error),
68 _ => Err(()),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
75pub struct Issue {
76 pub level: Level,
78 pub code: Option<String>,
80 pub message: String,
82 pub notes: Vec<String>,
84 pub help: Option<String>,
86 pub link: Option<String>,
88 pub annotations: Vec<Annotation>,
90 pub suggestions: Vec<(FileId, FixPlan)>,
92}
93
94#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
96pub struct IssueCollection {
97 issues: Vec<Issue>,
98}
99
100impl AnnotationKind {
101 #[inline]
103 pub const fn is_primary(&self) -> bool {
104 matches!(self, AnnotationKind::Primary)
105 }
106
107 #[inline]
109 pub const fn is_secondary(&self) -> bool {
110 matches!(self, AnnotationKind::Secondary)
111 }
112}
113
114impl Annotation {
115 pub fn new(kind: AnnotationKind, span: Span) -> Self {
132 Self { message: None, kind, span }
133 }
134
135 pub fn primary(span: Span) -> Self {
152 Self::new(AnnotationKind::Primary, span)
153 }
154
155 pub fn secondary(span: Span) -> Self {
172 Self::new(AnnotationKind::Secondary, span)
173 }
174
175 #[must_use]
192 pub fn with_message(mut self, message: impl Into<String>) -> Self {
193 self.message = Some(message.into());
194
195 self
196 }
197
198 pub fn is_primary(&self) -> bool {
200 self.kind == AnnotationKind::Primary
201 }
202}
203
204impl Level {
205 pub fn downgrade(&self) -> Self {
232 match self {
233 Level::Error => Level::Warning,
234 Level::Warning => Level::Help,
235 Level::Help | Level::Note => Level::Note,
236 }
237 }
238}
239
240impl Issue {
241 pub fn new(level: Level, message: impl Into<String>) -> Self {
251 Self {
252 level,
253 code: None,
254 message: message.into(),
255 annotations: Vec::new(),
256 notes: Vec::new(),
257 help: None,
258 link: None,
259 suggestions: Vec::new(),
260 }
261 }
262
263 pub fn error(message: impl Into<String>) -> Self {
273 Self::new(Level::Error, message)
274 }
275
276 pub fn warning(message: impl Into<String>) -> Self {
286 Self::new(Level::Warning, message)
287 }
288
289 pub fn help(message: impl Into<String>) -> Self {
299 Self::new(Level::Help, message)
300 }
301
302 pub fn note(message: impl Into<String>) -> Self {
312 Self::new(Level::Note, message)
313 }
314
315 #[must_use]
325 pub fn with_code(mut self, code: impl Into<String>) -> Self {
326 self.code = Some(code.into());
327
328 self
329 }
330
331 #[must_use]
349 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
350 self.annotations.push(annotation);
351
352 self
353 }
354
355 #[must_use]
356 pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
357 self.annotations.extend(annotation);
358
359 self
360 }
361
362 #[must_use]
372 pub fn with_note(mut self, note: impl Into<String>) -> Self {
373 self.notes.push(note.into());
374
375 self
376 }
377
378 #[must_use]
390 pub fn with_help(mut self, help: impl Into<String>) -> Self {
391 self.help = Some(help.into());
392
393 self
394 }
395
396 #[must_use]
406 pub fn with_link(mut self, link: impl Into<String>) -> Self {
407 self.link = Some(link.into());
408
409 self
410 }
411
412 #[must_use]
414 pub fn with_suggestion(mut self, file_id: FileId, plan: FixPlan) -> Self {
415 self.suggestions.push((file_id, plan));
416
417 self
418 }
419
420 #[must_use]
422 pub fn take_suggestions(&mut self) -> Vec<(FileId, FixPlan)> {
423 self.suggestions.drain(..).collect()
424 }
425}
426
427impl IssueCollection {
428 pub fn new() -> Self {
429 Self { issues: Vec::new() }
430 }
431
432 pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
433 Self { issues: issues.into_iter().collect() }
434 }
435
436 pub fn push(&mut self, issue: Issue) {
437 if self.issues.contains(&issue) {
438 return; }
440
441 self.issues.push(issue);
442 }
443
444 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
445 self.issues.extend(issues);
446 }
447
448 pub fn shrink_to_fit(&mut self) {
449 self.issues.shrink_to_fit();
450 }
451
452 pub fn is_empty(&self) -> bool {
453 self.issues.is_empty()
454 }
455
456 pub fn len(&self) -> usize {
457 self.issues.len()
458 }
459
460 pub fn with_maximum_level(self, level: Level) -> Self {
463 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
464 }
465
466 pub fn with_minimum_level(self, level: Level) -> Self {
469 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
470 }
471
472 pub fn has_minimum_level(&self, level: Level) -> bool {
475 self.issues.iter().any(|issue| issue.level >= level)
476 }
477
478 pub fn get_level_count(&self, level: Level) -> usize {
480 self.issues.iter().filter(|issue| issue.level == level).count()
481 }
482
483 pub fn get_highest_level(&self) -> Option<Level> {
485 self.issues.iter().map(|issue| issue.level).max()
486 }
487
488 pub fn filter_out_ignored(&mut self, ignore: &[String]) {
489 self.issues.retain(|issue| if let Some(code) = &issue.code { !ignore.contains(code) } else { true });
490 }
491
492 pub fn take_suggestions(&mut self) -> impl Iterator<Item = (FileId, FixPlan)> + '_ {
493 self.issues.iter_mut().flat_map(|issue| issue.take_suggestions())
494 }
495
496 pub fn only_fixable(self) -> impl Iterator<Item = Issue> {
497 self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty())
498 }
499
500 pub fn sorted(self) -> Self {
505 let mut issues = self.issues;
506
507 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
508 Ordering::Greater => Ordering::Greater,
509 Ordering::Less => Ordering::Less,
510 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
511 Ordering::Less => Ordering::Less,
512 Ordering::Greater => Ordering::Greater,
513 Ordering::Equal => {
514 let a_span = a
515 .annotations
516 .iter()
517 .find(|annotation| annotation.is_primary())
518 .map(|annotation| annotation.span);
519
520 let b_span = b
521 .annotations
522 .iter()
523 .find(|annotation| annotation.is_primary())
524 .map(|annotation| annotation.span);
525
526 match (a_span, b_span) {
527 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
528 (Some(_), None) => Ordering::Less,
529 (None, Some(_)) => Ordering::Greater,
530 (None, None) => Ordering::Equal,
531 }
532 }
533 },
534 });
535
536 Self { issues }
537 }
538
539 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
540 self.issues.iter()
541 }
542
543 pub fn to_fix_plans(self) -> HashMap<FileId, FixPlan> {
544 let mut plans: HashMap<FileId, FixPlan> = HashMap::default();
545 for issue in self.issues.into_iter().filter(|issue| !issue.suggestions.is_empty()) {
546 for suggestion in issue.suggestions.into_iter() {
547 match plans.entry(suggestion.0) {
548 Entry::Occupied(mut occupied_entry) => {
549 occupied_entry.get_mut().merge(suggestion.1);
550 }
551 Entry::Vacant(vacant_entry) => {
552 vacant_entry.insert(suggestion.1);
553 }
554 }
555 }
556 }
557
558 plans
559 }
560}
561
562impl IntoIterator for IssueCollection {
563 type Item = Issue;
564
565 type IntoIter = std::vec::IntoIter<Issue>;
566
567 fn into_iter(self) -> Self::IntoIter {
568 self.issues.into_iter()
569 }
570}
571
572impl Default for IssueCollection {
573 fn default() -> Self {
574 Self::new()
575 }
576}
577
578impl IntoIterator for Issue {
579 type Item = Issue;
580 type IntoIter = Once<Issue>;
581
582 fn into_iter(self) -> Self::IntoIter {
583 std::iter::once(self)
584 }
585}
586
587impl FromIterator<Issue> for IssueCollection {
588 fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
589 Self { issues: iter.into_iter().collect() }
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 pub fn test_highest_collection_level() {
599 let mut collection = IssueCollection::from(vec![]);
600 assert_eq!(collection.get_highest_level(), None);
601
602 collection.push(Issue::note("note"));
603 assert_eq!(collection.get_highest_level(), Some(Level::Note));
604
605 collection.push(Issue::help("help"));
606 assert_eq!(collection.get_highest_level(), Some(Level::Help));
607
608 collection.push(Issue::warning("warning"));
609 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
610
611 collection.push(Issue::error("error"));
612 assert_eq!(collection.get_highest_level(), Some(Level::Error));
613 }
614
615 #[test]
616 pub fn test_level_downgrade() {
617 assert_eq!(Level::Error.downgrade(), Level::Warning);
618 assert_eq!(Level::Warning.downgrade(), Level::Help);
619 assert_eq!(Level::Help.downgrade(), Level::Note);
620 assert_eq!(Level::Note.downgrade(), Level::Note);
621 }
622
623 #[test]
624 pub fn test_issue_collection_with_maximum_level() {
625 let mut collection = IssueCollection::from(vec![
626 Issue::error("error"),
627 Issue::warning("warning"),
628 Issue::help("help"),
629 Issue::note("note"),
630 ]);
631
632 collection = collection.with_maximum_level(Level::Warning);
633 assert_eq!(collection.len(), 3);
634 assert_eq!(
635 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
636 vec![Level::Warning, Level::Help, Level::Note]
637 );
638 }
639
640 #[test]
641 pub fn test_issue_collection_with_minimum_level() {
642 let mut collection = IssueCollection::from(vec![
643 Issue::error("error"),
644 Issue::warning("warning"),
645 Issue::help("help"),
646 Issue::note("note"),
647 ]);
648
649 collection = collection.with_minimum_level(Level::Warning);
650 assert_eq!(collection.len(), 2);
651 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
652 }
653
654 #[test]
655 pub fn test_issue_collection_has_minimum_level() {
656 let mut collection = IssueCollection::from(vec![]);
657
658 assert!(!collection.has_minimum_level(Level::Error));
659 assert!(!collection.has_minimum_level(Level::Warning));
660 assert!(!collection.has_minimum_level(Level::Help));
661 assert!(!collection.has_minimum_level(Level::Note));
662
663 collection.push(Issue::note("note"));
664
665 assert!(!collection.has_minimum_level(Level::Error));
666 assert!(!collection.has_minimum_level(Level::Warning));
667 assert!(!collection.has_minimum_level(Level::Help));
668 assert!(collection.has_minimum_level(Level::Note));
669
670 collection.push(Issue::help("help"));
671
672 assert!(!collection.has_minimum_level(Level::Error));
673 assert!(!collection.has_minimum_level(Level::Warning));
674 assert!(collection.has_minimum_level(Level::Help));
675 assert!(collection.has_minimum_level(Level::Note));
676
677 collection.push(Issue::warning("warning"));
678
679 assert!(!collection.has_minimum_level(Level::Error));
680 assert!(collection.has_minimum_level(Level::Warning));
681 assert!(collection.has_minimum_level(Level::Help));
682 assert!(collection.has_minimum_level(Level::Note));
683
684 collection.push(Issue::error("error"));
685
686 assert!(collection.has_minimum_level(Level::Error));
687 assert!(collection.has_minimum_level(Level::Warning));
688 assert!(collection.has_minimum_level(Level::Help));
689 assert!(collection.has_minimum_level(Level::Note));
690 }
691
692 #[test]
693 pub fn test_issue_collection_level_count() {
694 let mut collection = IssueCollection::from(vec![]);
695
696 assert_eq!(collection.get_level_count(Level::Error), 0);
697 assert_eq!(collection.get_level_count(Level::Warning), 0);
698 assert_eq!(collection.get_level_count(Level::Help), 0);
699 assert_eq!(collection.get_level_count(Level::Note), 0);
700
701 collection.push(Issue::error("error"));
702
703 assert_eq!(collection.get_level_count(Level::Error), 1);
704 assert_eq!(collection.get_level_count(Level::Warning), 0);
705 assert_eq!(collection.get_level_count(Level::Help), 0);
706 assert_eq!(collection.get_level_count(Level::Note), 0);
707
708 collection.push(Issue::warning("warning"));
709
710 assert_eq!(collection.get_level_count(Level::Error), 1);
711 assert_eq!(collection.get_level_count(Level::Warning), 1);
712 assert_eq!(collection.get_level_count(Level::Help), 0);
713 assert_eq!(collection.get_level_count(Level::Note), 0);
714
715 collection.push(Issue::help("help"));
716
717 assert_eq!(collection.get_level_count(Level::Error), 1);
718 assert_eq!(collection.get_level_count(Level::Warning), 1);
719 assert_eq!(collection.get_level_count(Level::Help), 1);
720 assert_eq!(collection.get_level_count(Level::Note), 0);
721
722 collection.push(Issue::note("note"));
723
724 assert_eq!(collection.get_level_count(Level::Error), 1);
725 assert_eq!(collection.get_level_count(Level::Warning), 1);
726 assert_eq!(collection.get_level_count(Level::Help), 1);
727 assert_eq!(collection.get_level_count(Level::Note), 1);
728 }
729}