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: IntoIterator<Item = Issue>>(iter: T) -> Self {
770 Self { issues: iter.into_iter().collect() }
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 #[test]
779 pub fn test_highest_collection_level() {
780 let mut collection = IssueCollection::from(vec![]);
781 assert_eq!(collection.get_highest_level(), None);
782
783 collection.push(Issue::note("note"));
784 assert_eq!(collection.get_highest_level(), Some(Level::Note));
785
786 collection.push(Issue::help("help"));
787 assert_eq!(collection.get_highest_level(), Some(Level::Help));
788
789 collection.push(Issue::warning("warning"));
790 assert_eq!(collection.get_highest_level(), Some(Level::Warning));
791
792 collection.push(Issue::error("error"));
793 assert_eq!(collection.get_highest_level(), Some(Level::Error));
794 }
795
796 #[test]
797 pub fn test_level_downgrade() {
798 assert_eq!(Level::Error.downgrade(), Level::Warning);
799 assert_eq!(Level::Warning.downgrade(), Level::Help);
800 assert_eq!(Level::Help.downgrade(), Level::Note);
801 assert_eq!(Level::Note.downgrade(), Level::Note);
802 }
803
804 #[test]
805 pub fn test_issue_collection_with_maximum_level() {
806 let mut collection = IssueCollection::from(vec![
807 Issue::error("error"),
808 Issue::warning("warning"),
809 Issue::help("help"),
810 Issue::note("note"),
811 ]);
812
813 collection = collection.with_maximum_level(Level::Warning);
814 assert_eq!(collection.len(), 3);
815 assert_eq!(
816 collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
817 vec![Level::Warning, Level::Help, Level::Note]
818 );
819 }
820
821 #[test]
822 pub fn test_issue_collection_with_minimum_level() {
823 let mut collection = IssueCollection::from(vec![
824 Issue::error("error"),
825 Issue::warning("warning"),
826 Issue::help("help"),
827 Issue::note("note"),
828 ]);
829
830 collection = collection.with_minimum_level(Level::Warning);
831 assert_eq!(collection.len(), 2);
832 assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
833 }
834
835 #[test]
836 pub fn test_issue_collection_has_minimum_level() {
837 let mut collection = IssueCollection::from(vec![]);
838
839 assert!(!collection.has_minimum_level(Level::Error));
840 assert!(!collection.has_minimum_level(Level::Warning));
841 assert!(!collection.has_minimum_level(Level::Help));
842 assert!(!collection.has_minimum_level(Level::Note));
843
844 collection.push(Issue::note("note"));
845
846 assert!(!collection.has_minimum_level(Level::Error));
847 assert!(!collection.has_minimum_level(Level::Warning));
848 assert!(!collection.has_minimum_level(Level::Help));
849 assert!(collection.has_minimum_level(Level::Note));
850
851 collection.push(Issue::help("help"));
852
853 assert!(!collection.has_minimum_level(Level::Error));
854 assert!(!collection.has_minimum_level(Level::Warning));
855 assert!(collection.has_minimum_level(Level::Help));
856 assert!(collection.has_minimum_level(Level::Note));
857
858 collection.push(Issue::warning("warning"));
859
860 assert!(!collection.has_minimum_level(Level::Error));
861 assert!(collection.has_minimum_level(Level::Warning));
862 assert!(collection.has_minimum_level(Level::Help));
863 assert!(collection.has_minimum_level(Level::Note));
864
865 collection.push(Issue::error("error"));
866
867 assert!(collection.has_minimum_level(Level::Error));
868 assert!(collection.has_minimum_level(Level::Warning));
869 assert!(collection.has_minimum_level(Level::Help));
870 assert!(collection.has_minimum_level(Level::Note));
871 }
872
873 #[test]
874 pub fn test_issue_collection_level_count() {
875 let mut collection = IssueCollection::from(vec![]);
876
877 assert_eq!(collection.get_level_count(Level::Error), 0);
878 assert_eq!(collection.get_level_count(Level::Warning), 0);
879 assert_eq!(collection.get_level_count(Level::Help), 0);
880 assert_eq!(collection.get_level_count(Level::Note), 0);
881
882 collection.push(Issue::error("error"));
883
884 assert_eq!(collection.get_level_count(Level::Error), 1);
885 assert_eq!(collection.get_level_count(Level::Warning), 0);
886 assert_eq!(collection.get_level_count(Level::Help), 0);
887 assert_eq!(collection.get_level_count(Level::Note), 0);
888
889 collection.push(Issue::warning("warning"));
890
891 assert_eq!(collection.get_level_count(Level::Error), 1);
892 assert_eq!(collection.get_level_count(Level::Warning), 1);
893 assert_eq!(collection.get_level_count(Level::Help), 0);
894 assert_eq!(collection.get_level_count(Level::Note), 0);
895
896 collection.push(Issue::help("help"));
897
898 assert_eq!(collection.get_level_count(Level::Error), 1);
899 assert_eq!(collection.get_level_count(Level::Warning), 1);
900 assert_eq!(collection.get_level_count(Level::Help), 1);
901 assert_eq!(collection.get_level_count(Level::Note), 0);
902
903 collection.push(Issue::note("note"));
904
905 assert_eq!(collection.get_level_count(Level::Error), 1);
906 assert_eq!(collection.get_level_count(Level::Warning), 1);
907 assert_eq!(collection.get_level_count(Level::Help), 1);
908 assert_eq!(collection.get_level_count(Level::Note), 1);
909 }
910
911 #[test]
912 pub fn test_primary_span_is_deterministic() {
913 let file = FileId::zero();
914 let span_later = Span::new(file, 20u32.into(), 25u32.into());
915 let span_earlier = Span::new(file, 5u32.into(), 10u32.into());
916
917 let issue = Issue::error("x")
918 .with_annotation(Annotation::primary(span_later))
919 .with_annotation(Annotation::primary(span_earlier));
920
921 assert_eq!(issue.primary_span(), Some(span_earlier));
922 }
923
924 fn ignore_fixture() -> (IssueCollection, std::collections::HashMap<FileId, &'static str>) {
925 let file_id = |name: &str| FileId::new(name);
926
927 let paths = ["src/App.php", "tests/Unit/FooTest.php", "modules/auth/views/login.tpl", "types/user/form.tpl"];
928
929 let mut mapping = std::collections::HashMap::new();
930 let issues: Vec<Issue> = paths
931 .iter()
932 .map(|p| {
933 let id = file_id(p);
934 mapping.insert(id, *p);
935 Issue::error("oops").with_code("invalid-global").with_annotation(Annotation::primary(Span::new(
936 id,
937 0u32.into(),
938 1u32.into(),
939 )))
940 })
941 .collect();
942
943 (IssueCollection::from(issues), mapping)
944 }
945
946 #[test]
947 pub fn test_filter_out_ignored_with_plain_prefix() {
948 let (mut collection, mapping) = ignore_fixture();
949 let ignore =
950 vec![IgnoreEntry::Scoped { code: "invalid-global".to_string(), paths: vec!["tests/".to_string()] }];
951
952 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| mapping.get(&id).map(|s| s.to_string()));
953
954 let remaining: Vec<String> = collection
955 .iter()
956 .filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
957 .map(String::from)
958 .collect();
959
960 assert_eq!(
961 remaining,
962 vec![
963 "src/App.php".to_string(),
964 "modules/auth/views/login.tpl".to_string(),
965 "types/user/form.tpl".to_string(),
966 ]
967 );
968 }
969
970 #[test]
971 pub fn test_filter_out_ignored_with_glob_pattern() {
972 let (mut collection, mapping) = ignore_fixture();
973 let ignore = vec![IgnoreEntry::Scoped {
974 code: "invalid-global".to_string(),
975 paths: vec!["modules/*/*/*.tpl".to_string(), "types/*/*.tpl".to_string()],
976 }];
977
978 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| mapping.get(&id).map(|s| s.to_string()));
979
980 let remaining: Vec<String> = collection
981 .iter()
982 .filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
983 .map(String::from)
984 .collect();
985
986 assert_eq!(remaining, vec!["src/App.php".to_string(), "tests/Unit/FooTest.php".to_string(),]);
987 }
988
989 #[test]
990 pub fn test_filter_out_ignored_mixes_plain_and_glob() {
991 let (mut collection, mapping) = ignore_fixture();
992 let ignore = vec![IgnoreEntry::Scoped {
993 code: "invalid-global".to_string(),
994 paths: vec!["tests/".to_string(), "modules/*/*/*.tpl".to_string(), "types/*/*.tpl".to_string()],
995 }];
996
997 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| mapping.get(&id).map(|s| s.to_string()));
998
999 let remaining: Vec<String> = collection
1000 .iter()
1001 .filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
1002 .map(String::from)
1003 .collect();
1004
1005 assert_eq!(remaining, vec!["src/App.php".to_string()]);
1006 }
1007
1008 #[test]
1009 pub fn test_filter_out_ignored_respects_code_scope() {
1010 let (mut collection, mapping) = ignore_fixture();
1011 let ignore = vec![IgnoreEntry::Scoped { code: "different-code".to_string(), paths: vec!["**/*".to_string()] }];
1012
1013 collection.filter_out_ignored(&ignore, GlobSettings::default(), |id| mapping.get(&id).map(|s| s.to_string()));
1014
1015 assert_eq!(collection.len(), 4);
1016 }
1017}