1#![allow(clippy::pub_use, clippy::exhaustive_enums)]
2
3use std::cmp::Ordering;
16use std::iter::Once;
17use std::str::FromStr;
18
19use foldhash::HashMap;
20use foldhash::HashMapExt;
21use schemars::JsonSchema;
22use serde::Deserialize;
23use serde::Serialize;
24use strum::Display;
25use strum::VariantNames;
26
27use mago_database::GlobSettings;
28use mago_database::file::FileId;
29use mago_database::matcher::ExclusionMatcher;
30use mago_span::Span;
31use mago_text_edit::TextEdit;
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
41#[serde(untagged)]
42pub enum IgnoreEntry {
43 Code(String),
45 Scoped {
48 code: String,
49 #[serde(rename = "in", deserialize_with = "one_or_many")]
50 paths: Vec<String>,
51 },
52}
53
54fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
55where
56 D: serde::Deserializer<'de>,
57{
58 #[derive(Deserialize)]
59 #[serde(untagged)]
60 enum OneOrMany {
61 One(String),
62 Many(Vec<String>),
63 }
64
65 match OneOrMany::deserialize(deserializer)? {
66 OneOrMany::One(s) => Ok(vec![s]),
67 OneOrMany::Many(v) => Ok(v),
68 }
69}
70
71mod formatter;
72mod internal;
73
74pub mod baseline;
75pub mod color;
76pub mod error;
77pub mod output;
78pub mod reporter;
79
80pub use color::ColorChoice;
81pub use formatter::ReportingFormat;
82pub use output::ReportingTarget;
83
84#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
86pub enum AnnotationKind {
87 Primary,
89 Secondary,
91}
92
93#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
95pub struct Annotation {
96 pub message: Option<String>,
98 pub kind: AnnotationKind,
100 pub span: Span,
102}
103
104#[derive(
106 Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames, JsonSchema,
107)]
108#[strum(serialize_all = "lowercase")]
109pub enum Level {
110 #[serde(alias = "note")]
112 Note,
113 #[serde(alias = "help")]
115 Help,
116 #[serde(alias = "warning", alias = "warn")]
118 Warning,
119 #[serde(alias = "error", alias = "err")]
121 Error,
122}
123
124impl FromStr for Level {
125 type Err = ();
126
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
128 match s.to_lowercase().as_str() {
129 "note" => Ok(Self::Note),
130 "help" => Ok(Self::Help),
131 "warning" => Ok(Self::Warning),
132 "error" => Ok(Self::Error),
133 _ => Err(()),
134 }
135 }
136}
137
138type IssueEdits = Vec<TextEdit>;
139type IssueEditBatches = Vec<(Option<String>, IssueEdits)>;
140
141#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
143pub struct Issue {
144 pub level: Level,
146 pub code: Option<String>,
148 pub message: String,
150 pub notes: Vec<String>,
152 pub help: Option<String>,
154 pub link: Option<String>,
156 pub annotations: Vec<Annotation>,
158 pub edits: HashMap<FileId, IssueEdits>,
160}
161
162#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
164pub struct IssueCollection {
165 issues: Vec<Issue>,
166}
167
168impl AnnotationKind {
169 #[inline]
171 #[must_use]
172 pub const fn is_primary(&self) -> bool {
173 matches!(self, AnnotationKind::Primary)
174 }
175
176 #[inline]
178 #[must_use]
179 pub const fn is_secondary(&self) -> bool {
180 matches!(self, AnnotationKind::Secondary)
181 }
182}
183
184impl Annotation {
185 #[must_use]
202 pub fn new(kind: AnnotationKind, span: Span) -> Self {
203 Self { message: None, kind, span }
204 }
205
206 #[must_use]
223 pub fn primary(span: Span) -> Self {
224 Self::new(AnnotationKind::Primary, span)
225 }
226
227 #[must_use]
244 pub fn secondary(span: Span) -> Self {
245 Self::new(AnnotationKind::Secondary, span)
246 }
247
248 #[must_use]
265 pub fn with_message(mut self, message: impl Into<String>) -> Self {
266 self.message = Some(message.into());
267
268 self
269 }
270
271 #[must_use]
273 pub fn is_primary(&self) -> bool {
274 self.kind == AnnotationKind::Primary
275 }
276}
277
278impl Level {
279 #[must_use]
306 pub fn downgrade(&self) -> Self {
307 match self {
308 Level::Error => Level::Warning,
309 Level::Warning => Level::Help,
310 Level::Help | Level::Note => Level::Note,
311 }
312 }
313}
314
315impl Issue {
316 pub fn new(level: Level, message: impl Into<String>) -> Self {
326 Self {
327 level,
328 code: None,
329 message: message.into(),
330 annotations: Vec::new(),
331 notes: Vec::new(),
332 help: None,
333 link: None,
334 edits: HashMap::default(),
335 }
336 }
337
338 pub fn error(message: impl Into<String>) -> Self {
348 Self::new(Level::Error, message)
349 }
350
351 pub fn warning(message: impl Into<String>) -> Self {
361 Self::new(Level::Warning, message)
362 }
363
364 pub fn help(message: impl Into<String>) -> Self {
374 Self::new(Level::Help, message)
375 }
376
377 pub fn note(message: impl Into<String>) -> Self {
387 Self::new(Level::Note, message)
388 }
389
390 #[must_use]
400 pub fn with_code(mut self, code: impl Into<String>) -> Self {
401 self.code = Some(code.into());
402
403 self
404 }
405
406 #[must_use]
424 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
425 self.annotations.push(annotation);
426
427 self
428 }
429
430 #[must_use]
431 pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
432 self.annotations.extend(annotation);
433
434 self
435 }
436
437 #[must_use]
441 pub fn primary_annotation(&self) -> Option<&Annotation> {
442 self.annotations.iter().filter(|annotation| annotation.is_primary()).min_by_key(|annotation| annotation.span)
443 }
444
445 #[must_use]
447 pub fn primary_span(&self) -> Option<Span> {
448 self.primary_annotation().map(|annotation| annotation.span)
449 }
450
451 #[must_use]
461 pub fn with_note(mut self, note: impl Into<String>) -> Self {
462 self.notes.push(note.into());
463
464 self
465 }
466
467 #[must_use]
479 pub fn with_help(mut self, help: impl Into<String>) -> Self {
480 self.help = Some(help.into());
481
482 self
483 }
484
485 #[must_use]
495 pub fn with_link(mut self, link: impl Into<String>) -> Self {
496 self.link = Some(link.into());
497
498 self
499 }
500
501 #[must_use]
503 pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
504 self.edits.entry(file_id).or_default().push(edit);
505
506 self
507 }
508
509 #[must_use]
511 pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
512 if !edits.is_empty() {
513 self.edits.entry(file_id).or_default().extend(edits);
514 }
515
516 self
517 }
518
519 #[must_use]
521 pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
522 std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
523 }
524}
525
526impl IssueCollection {
527 #[must_use]
528 pub fn new() -> Self {
529 Self { issues: Vec::new() }
530 }
531
532 pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
533 Self { issues: issues.into_iter().collect() }
534 }
535
536 pub fn push(&mut self, issue: Issue) {
537 self.issues.push(issue);
538 }
539
540 pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
541 self.issues.extend(issues);
542 }
543
544 pub fn reserve(&mut self, additional: usize) {
545 self.issues.reserve(additional);
546 }
547
548 pub fn shrink_to_fit(&mut self) {
549 self.issues.shrink_to_fit();
550 }
551
552 #[must_use]
553 pub fn is_empty(&self) -> bool {
554 self.issues.is_empty()
555 }
556
557 #[must_use]
558 pub fn len(&self) -> usize {
559 self.issues.len()
560 }
561
562 #[must_use]
565 pub fn with_maximum_level(self, level: Level) -> Self {
566 Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
567 }
568
569 #[must_use]
572 pub fn with_minimum_level(self, level: Level) -> Self {
573 Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
574 }
575
576 #[must_use]
579 pub fn has_minimum_level(&self, level: Level) -> bool {
580 self.issues.iter().any(|issue| issue.level >= level)
581 }
582
583 #[must_use]
585 pub fn get_level_count(&self, level: Level) -> usize {
586 self.issues.iter().filter(|issue| issue.level == level).count()
587 }
588
589 #[must_use]
591 pub fn get_highest_level(&self) -> Option<Level> {
592 self.issues.iter().map(|issue| issue.level).max()
593 }
594
595 #[must_use]
597 pub fn get_lowest_level(&self) -> Option<Level> {
598 self.issues.iter().map(|issue| issue.level).min()
599 }
600
601 pub fn filter_out_ignored<F>(&mut self, ignore: &[IgnoreEntry], glob: GlobSettings, resolve_file_name: F)
602 where
603 F: Fn(FileId) -> Option<String>,
604 {
605 if ignore.is_empty() {
606 return;
607 }
608
609 enum CompiledEntry<'entry> {
610 Code(&'entry str),
611 Scoped { code: &'entry str, matcher: ExclusionMatcher<&'entry str> },
612 }
613
614 let compiled: Vec<CompiledEntry<'_>> = ignore
615 .iter()
616 .filter_map(|entry| match entry {
617 IgnoreEntry::Code(code) => Some(CompiledEntry::Code(code.as_str())),
618 IgnoreEntry::Scoped { code, paths } => {
619 match ExclusionMatcher::compile(paths.iter().map(String::as_str), glob) {
620 Ok(matcher) => Some(CompiledEntry::Scoped { code: code.as_str(), matcher }),
621 Err(err) => {
622 tracing::error!(
623 "Failed to compile ignore patterns for `{code}`: {err}. Entry will be skipped."
624 );
625 None
626 }
627 }
628 }
629 })
630 .collect();
631
632 self.issues.retain(|issue| {
633 let Some(code) = &issue.code else {
634 return true;
635 };
636
637 let mut cached_path: Option<Option<String>> = None;
638
639 for entry in &compiled {
640 match entry {
641 CompiledEntry::Code(ignored_code) if *ignored_code == code => return false,
642 CompiledEntry::Scoped { code: ignored_code, matcher } if *ignored_code == code => {
643 let file_name = cached_path
644 .get_or_insert_with(|| {
645 issue.primary_span().and_then(|span| resolve_file_name(span.file_id))
646 })
647 .as_deref();
648
649 if let Some(name) = file_name
650 && matcher.is_match(name)
651 {
652 return false;
653 }
654 }
655 _ => {}
656 }
657 }
658
659 true
660 });
661 }
662
663 pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
664 self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
665 }
666
667 pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
668 self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
669 }
670
671 #[must_use]
673 pub fn with_edits(self) -> Self {
674 Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
675 }
676
677 #[must_use]
682 pub fn sorted(self) -> Self {
683 let mut issues = self.issues;
684
685 issues.sort_by(|a, b| match a.level.cmp(&b.level) {
686 Ordering::Greater => Ordering::Greater,
687 Ordering::Less => Ordering::Less,
688 Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
689 Ordering::Less => Ordering::Less,
690 Ordering::Greater => Ordering::Greater,
691 Ordering::Equal => {
692 let a_span = a.primary_span();
693 let b_span = b.primary_span();
694
695 match (a_span, b_span) {
696 (Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
697 (Some(_), None) => Ordering::Less,
698 (None, Some(_)) => Ordering::Greater,
699 (None, None) => Ordering::Equal,
700 }
701 }
702 },
703 });
704
705 Self { issues }
706 }
707
708 pub fn iter(&self) -> impl Iterator<Item = &Issue> {
709 self.issues.iter()
710 }
711
712 #[must_use]
720 pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
721 let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
722 for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
723 let code = issue.code;
724 for (file_id, edit_list) in issue.edits {
725 result.entry(file_id).or_default().push((code.clone(), edit_list));
726 }
727 }
728
729 result
730 }
731}
732
733impl IntoIterator for IssueCollection {
734 type Item = Issue;
735
736 type IntoIter = std::vec::IntoIter<Issue>;
737
738 fn into_iter(self) -> Self::IntoIter {
739 self.issues.into_iter()
740 }
741}
742
743impl<'collection> IntoIterator for &'collection IssueCollection {
744 type Item = &'collection Issue;
745
746 type IntoIter = std::slice::Iter<'collection, Issue>;
747
748 fn into_iter(self) -> Self::IntoIter {
749 self.issues.iter()
750 }
751}
752
753impl Default for IssueCollection {
754 fn default() -> Self {
755 Self::new()
756 }
757}
758
759impl IntoIterator for Issue {
760 type Item = Issue;
761 type IntoIter = Once<Issue>;
762
763 fn into_iter(self) -> Self::IntoIter {
764 std::iter::once(self)
765 }
766}
767
768impl FromIterator<Issue> for IssueCollection {
769 fn from_iter<T>(iter: T) -> Self
770 where
771 T: IntoIterator<Item = Issue>,
772 {
773 Self { issues: iter.into_iter().collect() }
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use std::collections::HashMap;
780
781 use super::*;
782
783 #[test]
784 pub fn test_highest_collection_level() {
785 let mut collection = IssueCollection::from(vec![]);
786 assert_eq!(collection.get_highest_level(), None);
787
788 collection.push(Issue::note("note"));
789 assert_eq!(collection.get_highest_level(), Some(Level::Note));
790
791 collection.push(Issue::help("help"));
792 assert_eq!(collection.get_highest_level(), Some(Level::Help));
793
794 collection.push(Issue::warning("warning"));
795 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
796
797 collection.push(Issue::error("error"));
798 assert_eq!(collection.get_highest_level(), Some(Level::Error));
799 }
800
801 #[test]
802 pub fn test_level_downgrade() {
803 assert_eq!(Level::Error.downgrade(), Level::Warning);
804 assert_eq!(Level::Warning.downgrade(), Level::Help);
805 assert_eq!(Level::Help.downgrade(), Level::Note);
806 assert_eq!(Level::Note.downgrade(), Level::Note);
807 }
808
809 #[test]
810 pub fn test_issue_collection_with_maximum_level() {
811 let mut collection = IssueCollection::from(vec![
812 Issue::error("error"),
813 Issue::warning("warning"),
814 Issue::help("help"),
815 Issue::note("note"),
816 ]);
817
818 collection = collection.with_maximum_level(Level::Warning);
819 assert_eq!(collection.len(), 3);
820 assert_eq!(
821 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
822 vec![Level::Warning, Level::Help, Level::Note]
823 );
824 }
825
826 #[test]
827 pub fn test_issue_collection_with_minimum_level() {
828 let mut collection = IssueCollection::from(vec![
829 Issue::error("error"),
830 Issue::warning("warning"),
831 Issue::help("help"),
832 Issue::note("note"),
833 ]);
834
835 collection = collection.with_minimum_level(Level::Warning);
836 assert_eq!(collection.len(), 2);
837 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
838 }
839
840 #[test]
841 pub fn test_issue_collection_has_minimum_level() {
842 let mut collection = IssueCollection::from(vec![]);
843
844 assert!(!collection.has_minimum_level(Level::Error));
845 assert!(!collection.has_minimum_level(Level::Warning));
846 assert!(!collection.has_minimum_level(Level::Help));
847 assert!(!collection.has_minimum_level(Level::Note));
848
849 collection.push(Issue::note("note"));
850
851 assert!(!collection.has_minimum_level(Level::Error));
852 assert!(!collection.has_minimum_level(Level::Warning));
853 assert!(!collection.has_minimum_level(Level::Help));
854 assert!(collection.has_minimum_level(Level::Note));
855
856 collection.push(Issue::help("help"));
857
858 assert!(!collection.has_minimum_level(Level::Error));
859 assert!(!collection.has_minimum_level(Level::Warning));
860 assert!(collection.has_minimum_level(Level::Help));
861 assert!(collection.has_minimum_level(Level::Note));
862
863 collection.push(Issue::warning("warning"));
864
865 assert!(!collection.has_minimum_level(Level::Error));
866 assert!(collection.has_minimum_level(Level::Warning));
867 assert!(collection.has_minimum_level(Level::Help));
868 assert!(collection.has_minimum_level(Level::Note));
869
870 collection.push(Issue::error("error"));
871
872 assert!(collection.has_minimum_level(Level::Error));
873 assert!(collection.has_minimum_level(Level::Warning));
874 assert!(collection.has_minimum_level(Level::Help));
875 assert!(collection.has_minimum_level(Level::Note));
876 }
877
878 #[test]
879 pub fn test_issue_collection_level_count() {
880 let mut collection = IssueCollection::from(vec![]);
881
882 assert_eq!(collection.get_level_count(Level::Error), 0);
883 assert_eq!(collection.get_level_count(Level::Warning), 0);
884 assert_eq!(collection.get_level_count(Level::Help), 0);
885 assert_eq!(collection.get_level_count(Level::Note), 0);
886
887 collection.push(Issue::error("error"));
888
889 assert_eq!(collection.get_level_count(Level::Error), 1);
890 assert_eq!(collection.get_level_count(Level::Warning), 0);
891 assert_eq!(collection.get_level_count(Level::Help), 0);
892 assert_eq!(collection.get_level_count(Level::Note), 0);
893
894 collection.push(Issue::warning("warning"));
895
896 assert_eq!(collection.get_level_count(Level::Error), 1);
897 assert_eq!(collection.get_level_count(Level::Warning), 1);
898 assert_eq!(collection.get_level_count(Level::Help), 0);
899 assert_eq!(collection.get_level_count(Level::Note), 0);
900
901 collection.push(Issue::help("help"));
902
903 assert_eq!(collection.get_level_count(Level::Error), 1);
904 assert_eq!(collection.get_level_count(Level::Warning), 1);
905 assert_eq!(collection.get_level_count(Level::Help), 1);
906 assert_eq!(collection.get_level_count(Level::Note), 0);
907
908 collection.push(Issue::note("note"));
909
910 assert_eq!(collection.get_level_count(Level::Error), 1);
911 assert_eq!(collection.get_level_count(Level::Warning), 1);
912 assert_eq!(collection.get_level_count(Level::Help), 1);
913 assert_eq!(collection.get_level_count(Level::Note), 1);
914 }
915
916 #[test]
917 pub fn test_primary_span_is_deterministic() {
918 let file = FileId::zero();
919 let span_later = Span::new(file, 20u32.into(), 25u32.into());
920 let span_earlier = Span::new(file, 5u32.into(), 10u32.into());
921
922 let issue = Issue::error("x")
923 .with_annotation(Annotation::primary(span_later))
924 .with_annotation(Annotation::primary(span_earlier));
925
926 assert_eq!(issue.primary_span(), Some(span_earlier));
927 }
928
929 fn ignore_fixture() -> (IssueCollection, HashMap<FileId, &'static [u8]>) {
930 let file_id = |name: &[u8]| FileId::new(name);
931
932 let paths: [&[u8]; 4] =
933 [b"src/App.php", b"tests/Unit/FooTest.php", b"modules/auth/views/login.tpl", b"types/user/form.tpl"];
934
935 let mut mapping = HashMap::new();
936 let issues: Vec<Issue> = paths
937 .iter()
938 .map(|p| {
939 let id = file_id(p);
940 mapping.insert(id, *p);
941 Issue::error("oops").with_code("invalid-global").with_annotation(Annotation::primary(Span::new(
942 id,
943 0u32.into(),
944 1u32.into(),
945 )))
946 })
947 .collect();
948
949 (IssueCollection::from(issues), mapping)
950 }
951
952 #[test]
953 pub fn test_filter_out_ignored_with_plain_prefix() {
954 let (mut collection, mapping) = ignore_fixture();
955 let ignore =
956 vec![IgnoreEntry::Scoped { code: "invalid-global".to_string(), paths: vec!["tests/".to_string()] }];
957
958 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| {
959 mapping.get(&id).map(|s| String::from_utf8_lossy(s).into_owned())
960 });
961
962 let remaining: Vec<String> = collection
963 .iter()
964 .filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
965 .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
966 .collect();
967
968 assert_eq!(
969 remaining,
970 vec![
971 "src/App.php".to_string(),
972 "modules/auth/views/login.tpl".to_string(),
973 "types/user/form.tpl".to_string(),
974 ]
975 );
976 }
977
978 #[test]
979 pub fn test_filter_out_ignored_with_glob_pattern() {
980 let (mut collection, mapping) = ignore_fixture();
981 let ignore = vec![IgnoreEntry::Scoped {
982 code: "invalid-global".to_string(),
983 paths: vec!["modules/*/*/*.tpl".to_string(), "types/*/*.tpl".to_string()],
984 }];
985
986 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| {
987 mapping.get(&id).map(|s| String::from_utf8_lossy(s).into_owned())
988 });
989
990 let remaining: Vec<String> = collection
991 .iter()
992 .filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
993 .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
994 .collect();
995
996 assert_eq!(remaining, vec!["src/App.php".to_string(), "tests/Unit/FooTest.php".to_string(),]);
997 }
998
999 #[test]
1000 pub fn test_filter_out_ignored_mixes_plain_and_glob() {
1001 let (mut collection, mapping) = ignore_fixture();
1002 let ignore = vec![IgnoreEntry::Scoped {
1003 code: "invalid-global".to_string(),
1004 paths: vec!["tests/".to_string(), "modules/*/*/*.tpl".to_string(), "types/*/*.tpl".to_string()],
1005 }];
1006
1007 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| {
1008 mapping.get(&id).map(|s| String::from_utf8_lossy(s).into_owned())
1009 });
1010
1011 let remaining: Vec<String> = collection
1012 .iter()
1013 .filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
1014 .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
1015 .collect();
1016
1017 assert_eq!(remaining, vec!["src/App.php".to_string()]);
1018 }
1019
1020 #[test]
1021 pub fn test_filter_out_ignored_respects_code_scope() {
1022 let (mut collection, mapping) = ignore_fixture();
1023 let ignore = vec![IgnoreEntry::Scoped { code: "different-code".to_string(), paths: vec!["**/*".to_string()] }];
1024
1025 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| {
1026 mapping.get(&id).map(|s| String::from_utf8_lossy(s).into_owned())
1027 });
1028
1029 assert_eq!(collection.len(), 4);
1030 }
1031}