1use oxc_span::Span;
4
5use crate::discover::FileId;
6use crate::suppress::{Suppression, UnknownSuppressionKind};
7
8#[derive(Debug, Clone)]
10pub struct ModuleInfo {
11 pub file_id: FileId,
13 pub exports: Vec<ExportInfo>,
15 pub imports: Vec<ImportInfo>,
17 pub re_exports: Vec<ReExportInfo>,
19 pub dynamic_imports: Vec<DynamicImportInfo>,
21 pub dynamic_import_patterns: Vec<DynamicImportPattern>,
23 pub require_calls: Vec<RequireCallInfo>,
25 pub member_accesses: Vec<MemberAccess>,
27 pub whole_object_uses: Vec<String>,
29 pub has_cjs_exports: bool,
31 pub has_angular_component_template_url: bool,
33 pub content_hash: u64,
35 pub suppressions: Vec<Suppression>,
37 pub unknown_suppression_kinds: Vec<UnknownSuppressionKind>,
42 pub unused_import_bindings: Vec<String>,
47 pub type_referenced_import_bindings: Vec<String>,
51 pub value_referenced_import_bindings: Vec<String>,
53 pub line_offsets: Vec<u32>,
55 pub complexity: Vec<FunctionComplexity>,
57 pub flag_uses: Vec<FlagUse>,
59 pub class_heritage: Vec<ClassHeritageInfo>,
61 pub local_type_declarations: Vec<LocalTypeDeclaration>,
63 pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
65 pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
67 pub iconify_prefixes: Vec<String>,
69 pub auto_import_candidates: Vec<String>,
71 pub directives: Vec<String>,
76 pub security_sinks: Vec<SinkSite>,
80 pub security_sinks_skipped: u32,
85 pub tainted_bindings: Vec<TaintedBinding>,
93 pub sanitized_sink_args: Vec<SanitizedSinkArg>,
97}
98
99#[derive(
102 Debug,
103 Clone,
104 Copy,
105 PartialEq,
106 Eq,
107 serde::Serialize,
108 serde::Deserialize,
109 bitcode::Encode,
110 bitcode::Decode,
111)]
112pub enum SanitizerScope {
113 Html,
115 Url,
117 Path,
119}
120
121#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
123pub struct SanitizedSinkArg {
124 pub span_start: u32,
126 pub arg_index: u32,
128 pub scope: SanitizerScope,
130}
131
132#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
141pub struct TaintedBinding {
142 pub local: String,
144 pub source_path: String,
146}
147
148#[derive(
153 Debug,
154 Clone,
155 Copy,
156 PartialEq,
157 Eq,
158 serde::Serialize,
159 serde::Deserialize,
160 bitcode::Encode,
161 bitcode::Decode,
162)]
163pub enum SinkShape {
164 Call,
166 MemberCall,
168 MemberAssign,
170 TaggedTemplate,
172 JsxAttr,
174}
175
176#[derive(
183 Debug,
184 Clone,
185 Copy,
186 PartialEq,
187 Eq,
188 serde::Serialize,
189 serde::Deserialize,
190 bitcode::Encode,
191 bitcode::Decode,
192)]
193pub enum SinkArgKind {
194 TemplateWithSubst,
198 Concat,
200 Object,
202 Call,
204 Other,
206}
207
208#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
213pub struct SinkSite {
214 pub sink_shape: SinkShape,
216 pub callee_path: String,
218 pub arg_index: u32,
220 pub arg_is_non_literal: bool,
222 pub arg_kind: SinkArgKind,
227 pub arg_idents: Vec<String>,
235 pub span_start: u32,
238 pub span_end: u32,
240}
241
242impl SinkSite {
243 #[must_use]
245 pub fn span(&self) -> Span {
246 Span::new(self.span_start, self.span_end)
247 }
248}
249
250#[derive(Debug, Clone)]
252pub struct NamespaceObjectAlias {
253 pub via_export_name: String,
255 pub suffix: String,
257 pub namespace_local: String,
259}
260
261#[must_use]
263#[expect(
264 clippy::cast_possible_truncation,
265 reason = "source files are practically < 4GB"
266)]
267pub fn compute_line_offsets(source: &str) -> Vec<u32> {
268 let mut offsets = vec![0u32];
269 for (i, byte) in source.bytes().enumerate() {
270 if byte == b'\n' {
271 debug_assert!(
272 u32::try_from(i + 1).is_ok(),
273 "source file exceeds u32::MAX bytes — line offsets would overflow"
274 );
275 offsets.push((i + 1) as u32);
276 }
277 }
278 offsets
279}
280
281#[must_use]
283#[expect(
284 clippy::cast_possible_truncation,
285 reason = "line count is bounded by source size"
286)]
287pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
288 let line_idx = match line_offsets.binary_search(&byte_offset) {
289 Ok(idx) => idx,
290 Err(idx) => idx.saturating_sub(1),
291 };
292 let line = line_idx as u32 + 1;
293 let col = byte_offset - line_offsets[line_idx];
294 (line, col)
295}
296
297#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
299pub struct FunctionComplexity {
300 pub name: String,
302 pub line: u32,
304 pub col: u32,
306 pub cyclomatic: u16,
308 pub cognitive: u16,
310 pub line_count: u32,
312 pub param_count: u8,
314 pub source_hash: Option<String>,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
320pub enum FlagUseKind {
321 EnvVar,
323 SdkCall,
325 ConfigObject,
327}
328
329#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
331pub struct FlagUse {
332 pub flag_name: String,
334 pub kind: FlagUseKind,
336 pub line: u32,
338 pub col: u32,
340 pub guard_span_start: Option<u32>,
342 pub guard_span_end: Option<u32>,
344 pub sdk_name: Option<String>,
346}
347
348const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
349
350#[derive(Debug, Clone)]
352pub struct DynamicImportPattern {
353 pub prefix: String,
355 pub suffix: Option<String>,
357 pub span: Span,
359}
360
361#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
363#[serde(rename_all = "lowercase")]
364#[repr(u8)]
365pub enum VisibilityTag {
366 #[default]
368 None = 0,
369 Public = 1,
371 Internal = 2,
373 Beta = 3,
375 Alpha = 4,
377 ExpectedUnused = 5,
379}
380
381impl VisibilityTag {
382 pub const fn suppresses_unused(self) -> bool {
386 matches!(
387 self,
388 Self::Public | Self::Internal | Self::Beta | Self::Alpha
389 )
390 }
391
392 pub fn is_none(&self) -> bool {
394 matches!(self, Self::None)
395 }
396}
397
398#[derive(Debug, Clone, serde::Serialize)]
400pub struct ExportInfo {
401 pub name: ExportName,
403 pub local_name: Option<String>,
405 pub is_type_only: bool,
407 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
409 pub is_side_effect_used: bool,
410 #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
412 pub visibility: VisibilityTag,
413 #[serde(serialize_with = "serialize_span")]
415 pub span: Span,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub members: Vec<MemberInfo>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub super_class: Option<String>,
422}
423
424#[derive(
426 Debug,
427 Clone,
428 serde::Serialize,
429 serde::Deserialize,
430 bitcode::Encode,
431 bitcode::Decode,
432 PartialEq,
433 Eq,
434)]
435pub struct ClassHeritageInfo {
436 pub export_name: String,
438 pub super_class: Option<String>,
440 pub implements: Vec<String>,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
444 pub instance_bindings: Vec<(String, String)>,
445}
446
447#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
449pub struct LocalTypeDeclaration {
450 pub name: String,
452 #[serde(serialize_with = "serialize_span")]
454 pub span: Span,
455}
456
457#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
459pub struct PublicSignatureTypeReference {
460 pub export_name: String,
462 pub type_name: String,
464 #[serde(serialize_with = "serialize_span")]
466 pub span: Span,
467}
468
469#[derive(Debug, Clone, serde::Serialize)]
471pub struct MemberInfo {
472 pub name: String,
474 pub kind: MemberKind,
476 #[serde(serialize_with = "serialize_span")]
478 pub span: Span,
479 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
484 pub has_decorator: bool,
485 #[serde(default, skip_serializing_if = "Vec::is_empty")]
492 pub decorator_names: Vec<String>,
493 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
500 pub is_instance_returning_static: bool,
501 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
509 pub is_self_returning: bool,
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
514#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
515#[serde(rename_all = "snake_case")]
516pub enum MemberKind {
517 EnumMember,
519 ClassMethod,
521 ClassProperty,
523 NamespaceMember,
525}
526
527#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
529pub struct MemberAccess {
530 pub object: String,
532 pub member: String,
534}
535
536#[expect(
537 clippy::trivially_copy_pass_by_ref,
538 reason = "serde serialize_with requires &T"
539)]
540fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
541 use serde::ser::SerializeMap;
542 let mut map = serializer.serialize_map(Some(2))?;
543 map.serialize_entry("start", &span.start)?;
544 map.serialize_entry("end", &span.end)?;
545 map.end()
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
550pub enum ExportName {
551 Named(String),
553 Default,
555}
556
557impl ExportName {
558 #[must_use]
560 pub fn matches_str(&self, s: &str) -> bool {
561 match self {
562 Self::Named(n) => n == s,
563 Self::Default => s == "default",
564 }
565 }
566}
567
568impl std::fmt::Display for ExportName {
569 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 match self {
571 Self::Named(n) => write!(f, "{n}"),
572 Self::Default => write!(f, "default"),
573 }
574 }
575}
576
577#[derive(Debug, Clone)]
579pub struct ImportInfo {
580 pub source: String,
582 pub imported_name: ImportedName,
584 pub local_name: String,
586 pub is_type_only: bool,
588 pub from_style: bool,
590 pub span: Span,
592 pub source_span: Span,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq)]
598pub enum ImportedName {
599 Named(String),
601 Default,
603 Namespace,
605 SideEffect,
607}
608
609#[cfg(target_pointer_width = "64")]
610const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
611#[cfg(target_pointer_width = "64")]
612const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
613#[cfg(target_pointer_width = "64")]
614const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
615#[cfg(target_pointer_width = "64")]
616const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
617#[cfg(target_pointer_width = "64")]
618const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
619#[cfg(target_pointer_width = "64")]
620const _: () = assert!(std::mem::size_of::<SinkSite>() == 64);
621#[cfg(target_pointer_width = "64")]
622const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 648);
623
624#[derive(Debug, Clone)]
626pub struct ReExportInfo {
627 pub source: String,
629 pub imported_name: String,
631 pub exported_name: String,
633 pub is_type_only: bool,
635 pub span: oxc_span::Span,
637}
638
639#[derive(Debug, Clone)]
641pub struct DynamicImportInfo {
642 pub source: String,
644 pub span: Span,
646 pub destructured_names: Vec<String>,
650 pub local_name: Option<String>,
653 pub is_speculative: bool,
655}
656
657#[derive(Debug, Clone)]
659pub struct RequireCallInfo {
660 pub source: String,
662 pub span: Span,
664 pub destructured_names: Vec<String>,
666 pub local_name: Option<String>,
668}
669
670pub struct ParseResult {
672 pub modules: Vec<ModuleInfo>,
674 pub cache_hits: usize,
676 pub cache_misses: usize,
678 pub parse_cpu_ms: f64,
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn line_offsets_empty_string() {
688 assert_eq!(compute_line_offsets(""), vec![0]);
689 }
690
691 #[test]
692 fn sink_shape_bitcode_roundtrip() {
693 for shape in [
694 SinkShape::Call,
695 SinkShape::MemberCall,
696 SinkShape::MemberAssign,
697 SinkShape::TaggedTemplate,
698 SinkShape::JsxAttr,
699 ] {
700 let encoded = bitcode::encode(&shape);
701 let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
702 assert_eq!(shape, decoded);
703 }
704 }
705
706 #[test]
707 fn sink_arg_kind_bitcode_roundtrip() {
708 for kind in [
709 SinkArgKind::TemplateWithSubst,
710 SinkArgKind::Concat,
711 SinkArgKind::Object,
712 SinkArgKind::Call,
713 SinkArgKind::Other,
714 ] {
715 let encoded = bitcode::encode(&kind);
716 let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
717 assert_eq!(kind, decoded);
718 }
719 }
720
721 #[test]
722 fn sink_site_bitcode_roundtrip() {
723 let site = SinkSite {
724 sink_shape: SinkShape::MemberAssign,
725 callee_path: "el.innerHTML".to_string(),
726 arg_index: 0,
727 arg_is_non_literal: true,
728 arg_kind: SinkArgKind::Other,
729 arg_idents: vec!["userInput".to_string()],
730 span_start: 10,
731 span_end: 20,
732 };
733 let encoded = bitcode::encode(&site);
734 let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
735 assert_eq!(decoded.sink_shape, site.sink_shape);
736 assert_eq!(decoded.callee_path, site.callee_path);
737 assert_eq!(decoded.arg_index, site.arg_index);
738 assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
739 assert_eq!(decoded.arg_kind, site.arg_kind);
740 assert_eq!(decoded.arg_idents, site.arg_idents);
741 assert_eq!(decoded.span(), site.span());
742 }
743
744 #[test]
745 fn line_offsets_single_line_no_newline() {
746 assert_eq!(compute_line_offsets("hello"), vec![0]);
747 }
748
749 #[test]
750 fn line_offsets_single_line_with_newline() {
751 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
752 }
753
754 #[test]
755 fn line_offsets_multiple_lines() {
756 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
757 }
758
759 #[test]
760 fn line_offsets_trailing_newline() {
761 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
762 }
763
764 #[test]
765 fn line_offsets_consecutive_newlines() {
766 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
767 }
768
769 #[test]
770 fn line_offsets_multibyte_utf8() {
771 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
772 }
773
774 #[test]
775 fn line_col_offset_zero() {
776 let offsets = compute_line_offsets("abc\ndef\nghi");
777 let (line, col) = byte_offset_to_line_col(&offsets, 0);
778 assert_eq!((line, col), (1, 0));
779 }
780
781 #[test]
782 fn line_col_middle_of_first_line() {
783 let offsets = compute_line_offsets("abc\ndef\nghi");
784 let (line, col) = byte_offset_to_line_col(&offsets, 2);
785 assert_eq!((line, col), (1, 2));
786 }
787
788 #[test]
789 fn line_col_start_of_second_line() {
790 let offsets = compute_line_offsets("abc\ndef\nghi");
791 let (line, col) = byte_offset_to_line_col(&offsets, 4);
792 assert_eq!((line, col), (2, 0));
793 }
794
795 #[test]
796 fn line_col_middle_of_second_line() {
797 let offsets = compute_line_offsets("abc\ndef\nghi");
798 let (line, col) = byte_offset_to_line_col(&offsets, 5);
799 assert_eq!((line, col), (2, 1));
800 }
801
802 #[test]
803 fn line_col_start_of_third_line() {
804 let offsets = compute_line_offsets("abc\ndef\nghi");
805 let (line, col) = byte_offset_to_line_col(&offsets, 8);
806 assert_eq!((line, col), (3, 0));
807 }
808
809 #[test]
810 fn line_col_end_of_file() {
811 let offsets = compute_line_offsets("abc\ndef\nghi");
812 let (line, col) = byte_offset_to_line_col(&offsets, 10);
813 assert_eq!((line, col), (3, 2));
814 }
815
816 #[test]
817 fn line_col_single_line() {
818 let offsets = compute_line_offsets("hello");
819 let (line, col) = byte_offset_to_line_col(&offsets, 3);
820 assert_eq!((line, col), (1, 3));
821 }
822
823 #[test]
824 fn line_col_at_newline_byte() {
825 let offsets = compute_line_offsets("abc\ndef");
826 let (line, col) = byte_offset_to_line_col(&offsets, 3);
827 assert_eq!((line, col), (1, 3));
828 }
829
830 #[test]
831 fn export_name_matches_str_named() {
832 let name = ExportName::Named("foo".to_string());
833 assert!(name.matches_str("foo"));
834 assert!(!name.matches_str("bar"));
835 assert!(!name.matches_str("default"));
836 }
837
838 #[test]
839 fn export_name_matches_str_default() {
840 let name = ExportName::Default;
841 assert!(name.matches_str("default"));
842 assert!(!name.matches_str("foo"));
843 }
844
845 #[test]
846 fn export_name_display_named() {
847 let name = ExportName::Named("myExport".to_string());
848 assert_eq!(name.to_string(), "myExport");
849 }
850
851 #[test]
852 fn export_name_display_default() {
853 let name = ExportName::Default;
854 assert_eq!(name.to_string(), "default");
855 }
856
857 #[test]
858 fn export_name_equality_named() {
859 let a = ExportName::Named("foo".to_string());
860 let b = ExportName::Named("foo".to_string());
861 let c = ExportName::Named("bar".to_string());
862 assert_eq!(a, b);
863 assert_ne!(a, c);
864 }
865
866 #[test]
867 fn export_name_equality_default() {
868 let a = ExportName::Default;
869 let b = ExportName::Default;
870 assert_eq!(a, b);
871 }
872
873 #[test]
874 fn export_name_named_not_equal_to_default() {
875 let named = ExportName::Named("default".to_string());
876 let default = ExportName::Default;
877 assert_ne!(named, default);
878 }
879
880 #[test]
881 fn export_name_hash_consistency() {
882 use std::collections::hash_map::DefaultHasher;
883 use std::hash::{Hash, Hasher};
884
885 let mut h1 = DefaultHasher::new();
886 let mut h2 = DefaultHasher::new();
887 ExportName::Named("foo".to_string()).hash(&mut h1);
888 ExportName::Named("foo".to_string()).hash(&mut h2);
889 assert_eq!(h1.finish(), h2.finish());
890 }
891
892 #[test]
893 fn export_name_matches_str_empty_string() {
894 let name = ExportName::Named(String::new());
895 assert!(name.matches_str(""));
896 assert!(!name.matches_str("foo"));
897 }
898
899 #[test]
900 fn export_name_default_does_not_match_empty() {
901 let name = ExportName::Default;
902 assert!(!name.matches_str(""));
903 }
904
905 #[test]
906 fn imported_name_equality() {
907 assert_eq!(
908 ImportedName::Named("foo".to_string()),
909 ImportedName::Named("foo".to_string())
910 );
911 assert_ne!(
912 ImportedName::Named("foo".to_string()),
913 ImportedName::Named("bar".to_string())
914 );
915 assert_eq!(ImportedName::Default, ImportedName::Default);
916 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
917 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
918 assert_ne!(ImportedName::Default, ImportedName::Namespace);
919 assert_ne!(
920 ImportedName::Named("default".to_string()),
921 ImportedName::Default
922 );
923 }
924
925 #[test]
926 fn member_kind_equality() {
927 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
928 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
929 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
930 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
931 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
932 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
933 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
934 }
935
936 #[test]
937 fn member_kind_bitcode_roundtrip() {
938 let kinds = [
939 MemberKind::EnumMember,
940 MemberKind::ClassMethod,
941 MemberKind::ClassProperty,
942 MemberKind::NamespaceMember,
943 ];
944 for kind in &kinds {
945 let bytes = bitcode::encode(kind);
946 let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
947 assert_eq!(&decoded, kind);
948 }
949 }
950
951 #[test]
952 fn member_access_bitcode_roundtrip() {
953 let access = MemberAccess {
954 object: "Status".to_string(),
955 member: "Active".to_string(),
956 };
957 let bytes = bitcode::encode(&access);
958 let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
959 assert_eq!(decoded.object, "Status");
960 assert_eq!(decoded.member, "Active");
961 }
962
963 #[test]
964 fn line_offsets_crlf_only_counts_lf() {
965 let offsets = compute_line_offsets("ab\r\ncd");
966 assert_eq!(offsets, vec![0, 4]);
967 }
968
969 #[test]
970 fn line_col_empty_file_offset_zero() {
971 let offsets = compute_line_offsets("");
972 let (line, col) = byte_offset_to_line_col(&offsets, 0);
973 assert_eq!((line, col), (1, 0));
974 }
975
976 #[test]
977 fn function_complexity_bitcode_roundtrip() {
978 let fc = FunctionComplexity {
979 name: "processData".to_string(),
980 line: 42,
981 col: 4,
982 cyclomatic: 15,
983 cognitive: 25,
984 line_count: 80,
985 param_count: 3,
986 source_hash: Some("0123456789abcdef".to_string()),
987 };
988 let bytes = bitcode::encode(&fc);
989 let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
990 assert_eq!(decoded.name, "processData");
991 assert_eq!(decoded.line, 42);
992 assert_eq!(decoded.col, 4);
993 assert_eq!(decoded.cyclomatic, 15);
994 assert_eq!(decoded.cognitive, 25);
995 assert_eq!(decoded.line_count, 80);
996 assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
997 }
998}