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 injection_tokens: Vec<(String, String)>,
69 pub local_type_declarations: Vec<LocalTypeDeclaration>,
71 pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
73 pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
75 pub iconify_prefixes: Vec<String>,
77 pub auto_import_candidates: Vec<String>,
79 pub directives: Vec<String>,
84 pub security_sinks: Vec<SinkSite>,
88 pub security_sinks_skipped: u32,
93 pub tainted_bindings: Vec<TaintedBinding>,
101 pub sanitized_sink_args: Vec<SanitizedSinkArg>,
105}
106
107#[derive(
110 Debug,
111 Clone,
112 Copy,
113 PartialEq,
114 Eq,
115 serde::Serialize,
116 serde::Deserialize,
117 bitcode::Encode,
118 bitcode::Decode,
119)]
120pub enum SanitizerScope {
121 Html,
123 Url,
125 Path,
127}
128
129#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
131pub struct SanitizedSinkArg {
132 pub span_start: u32,
134 pub arg_index: u32,
136 pub scope: SanitizerScope,
138}
139
140#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
149pub struct TaintedBinding {
150 pub local: String,
152 pub source_path: String,
154}
155
156#[derive(
161 Debug,
162 Clone,
163 Copy,
164 PartialEq,
165 Eq,
166 serde::Serialize,
167 serde::Deserialize,
168 bitcode::Encode,
169 bitcode::Decode,
170)]
171pub enum SinkShape {
172 Call,
174 MemberCall,
176 MemberAssign,
178 TaggedTemplate,
180 JsxAttr,
182}
183
184#[derive(
191 Debug,
192 Clone,
193 Copy,
194 PartialEq,
195 Eq,
196 serde::Serialize,
197 serde::Deserialize,
198 bitcode::Encode,
199 bitcode::Decode,
200)]
201pub enum SinkArgKind {
202 TemplateWithSubst,
206 Concat,
208 Object,
210 Call,
212 Other,
214}
215
216#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
221pub struct SinkSite {
222 pub sink_shape: SinkShape,
224 pub callee_path: String,
226 pub arg_index: u32,
228 pub arg_is_non_literal: bool,
230 pub arg_kind: SinkArgKind,
235 pub arg_idents: Vec<String>,
243 pub span_start: u32,
246 pub span_end: u32,
248}
249
250impl SinkSite {
251 #[must_use]
253 pub fn span(&self) -> Span {
254 Span::new(self.span_start, self.span_end)
255 }
256}
257
258#[derive(Debug, Clone)]
260pub struct NamespaceObjectAlias {
261 pub via_export_name: String,
263 pub suffix: String,
265 pub namespace_local: String,
267}
268
269#[must_use]
271#[expect(
272 clippy::cast_possible_truncation,
273 reason = "source files are practically < 4GB"
274)]
275pub fn compute_line_offsets(source: &str) -> Vec<u32> {
276 let mut offsets = vec![0u32];
277 for (i, byte) in source.bytes().enumerate() {
278 if byte == b'\n' {
279 debug_assert!(
280 u32::try_from(i + 1).is_ok(),
281 "source file exceeds u32::MAX bytes — line offsets would overflow"
282 );
283 offsets.push((i + 1) as u32);
284 }
285 }
286 offsets
287}
288
289#[must_use]
291#[expect(
292 clippy::cast_possible_truncation,
293 reason = "line count is bounded by source size"
294)]
295pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
296 let line_idx = match line_offsets.binary_search(&byte_offset) {
297 Ok(idx) => idx,
298 Err(idx) => idx.saturating_sub(1),
299 };
300 let line = line_idx as u32 + 1;
301 let col = byte_offset - line_offsets[line_idx];
302 (line, col)
303}
304
305#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
307pub struct FunctionComplexity {
308 pub name: String,
310 pub line: u32,
312 pub col: u32,
314 pub cyclomatic: u16,
316 pub cognitive: u16,
318 pub line_count: u32,
320 pub param_count: u8,
322 pub source_hash: Option<String>,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
328pub enum FlagUseKind {
329 EnvVar,
331 SdkCall,
333 ConfigObject,
335}
336
337#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
339pub struct FlagUse {
340 pub flag_name: String,
342 pub kind: FlagUseKind,
344 pub line: u32,
346 pub col: u32,
348 pub guard_span_start: Option<u32>,
350 pub guard_span_end: Option<u32>,
352 pub sdk_name: Option<String>,
354}
355
356const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
357
358#[derive(Debug, Clone)]
360pub struct DynamicImportPattern {
361 pub prefix: String,
363 pub suffix: Option<String>,
365 pub span: Span,
367}
368
369#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
371#[serde(rename_all = "lowercase")]
372#[repr(u8)]
373pub enum VisibilityTag {
374 #[default]
376 None = 0,
377 Public = 1,
379 Internal = 2,
381 Beta = 3,
383 Alpha = 4,
385 ExpectedUnused = 5,
387}
388
389impl VisibilityTag {
390 pub const fn suppresses_unused(self) -> bool {
394 matches!(
395 self,
396 Self::Public | Self::Internal | Self::Beta | Self::Alpha
397 )
398 }
399
400 pub fn is_none(&self) -> bool {
402 matches!(self, Self::None)
403 }
404}
405
406#[derive(Debug, Clone, serde::Serialize)]
408pub struct ExportInfo {
409 pub name: ExportName,
411 pub local_name: Option<String>,
413 pub is_type_only: bool,
415 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
417 pub is_side_effect_used: bool,
418 #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
420 pub visibility: VisibilityTag,
421 #[serde(serialize_with = "serialize_span")]
423 pub span: Span,
424 #[serde(default, skip_serializing_if = "Vec::is_empty")]
426 pub members: Vec<MemberInfo>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub super_class: Option<String>,
430}
431
432#[derive(
434 Debug,
435 Clone,
436 serde::Serialize,
437 serde::Deserialize,
438 bitcode::Encode,
439 bitcode::Decode,
440 PartialEq,
441 Eq,
442)]
443pub struct ClassHeritageInfo {
444 pub export_name: String,
446 pub super_class: Option<String>,
448 pub implements: Vec<String>,
450 #[serde(default, skip_serializing_if = "Vec::is_empty")]
452 pub instance_bindings: Vec<(String, String)>,
453}
454
455#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
457pub struct LocalTypeDeclaration {
458 pub name: String,
460 #[serde(serialize_with = "serialize_span")]
462 pub span: Span,
463}
464
465#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
467pub struct PublicSignatureTypeReference {
468 pub export_name: String,
470 pub type_name: String,
472 #[serde(serialize_with = "serialize_span")]
474 pub span: Span,
475}
476
477#[derive(Debug, Clone, serde::Serialize)]
479pub struct MemberInfo {
480 pub name: String,
482 pub kind: MemberKind,
484 #[serde(serialize_with = "serialize_span")]
486 pub span: Span,
487 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
492 pub has_decorator: bool,
493 #[serde(default, skip_serializing_if = "Vec::is_empty")]
500 pub decorator_names: Vec<String>,
501 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
508 pub is_instance_returning_static: bool,
509 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
517 pub is_self_returning: bool,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
523#[serde(rename_all = "snake_case")]
524pub enum MemberKind {
525 EnumMember,
527 ClassMethod,
529 ClassProperty,
531 NamespaceMember,
533}
534
535#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
537pub struct MemberAccess {
538 pub object: String,
540 pub member: String,
542}
543
544#[expect(
545 clippy::trivially_copy_pass_by_ref,
546 reason = "serde serialize_with requires &T"
547)]
548fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
549 use serde::ser::SerializeMap;
550 let mut map = serializer.serialize_map(Some(2))?;
551 map.serialize_entry("start", &span.start)?;
552 map.serialize_entry("end", &span.end)?;
553 map.end()
554}
555
556#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
558pub enum ExportName {
559 Named(String),
561 Default,
563}
564
565impl ExportName {
566 #[must_use]
568 pub fn matches_str(&self, s: &str) -> bool {
569 match self {
570 Self::Named(n) => n == s,
571 Self::Default => s == "default",
572 }
573 }
574}
575
576impl std::fmt::Display for ExportName {
577 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578 match self {
579 Self::Named(n) => write!(f, "{n}"),
580 Self::Default => write!(f, "default"),
581 }
582 }
583}
584
585#[derive(Debug, Clone)]
587pub struct ImportInfo {
588 pub source: String,
590 pub imported_name: ImportedName,
592 pub local_name: String,
594 pub is_type_only: bool,
596 pub from_style: bool,
598 pub span: Span,
600 pub source_span: Span,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq)]
606pub enum ImportedName {
607 Named(String),
609 Default,
611 Namespace,
613 SideEffect,
615}
616
617#[cfg(target_pointer_width = "64")]
618const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
619#[cfg(target_pointer_width = "64")]
620const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
621#[cfg(target_pointer_width = "64")]
622const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
623#[cfg(target_pointer_width = "64")]
624const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
625#[cfg(target_pointer_width = "64")]
626const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
627#[cfg(target_pointer_width = "64")]
628const _: () = assert!(std::mem::size_of::<SinkSite>() == 64);
629#[cfg(target_pointer_width = "64")]
630const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 672);
631
632#[derive(Debug, Clone)]
634pub struct ReExportInfo {
635 pub source: String,
637 pub imported_name: String,
639 pub exported_name: String,
641 pub is_type_only: bool,
643 pub span: oxc_span::Span,
645}
646
647#[derive(Debug, Clone)]
649pub struct DynamicImportInfo {
650 pub source: String,
652 pub span: Span,
654 pub destructured_names: Vec<String>,
658 pub local_name: Option<String>,
661 pub is_speculative: bool,
663}
664
665#[derive(Debug, Clone)]
667pub struct RequireCallInfo {
668 pub source: String,
670 pub span: Span,
672 pub destructured_names: Vec<String>,
674 pub local_name: Option<String>,
676}
677
678pub struct ParseResult {
680 pub modules: Vec<ModuleInfo>,
682 pub cache_hits: usize,
684 pub cache_misses: usize,
686 pub parse_cpu_ms: f64,
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn line_offsets_empty_string() {
696 assert_eq!(compute_line_offsets(""), vec![0]);
697 }
698
699 #[test]
700 fn sink_shape_bitcode_roundtrip() {
701 for shape in [
702 SinkShape::Call,
703 SinkShape::MemberCall,
704 SinkShape::MemberAssign,
705 SinkShape::TaggedTemplate,
706 SinkShape::JsxAttr,
707 ] {
708 let encoded = bitcode::encode(&shape);
709 let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
710 assert_eq!(shape, decoded);
711 }
712 }
713
714 #[test]
715 fn sink_arg_kind_bitcode_roundtrip() {
716 for kind in [
717 SinkArgKind::TemplateWithSubst,
718 SinkArgKind::Concat,
719 SinkArgKind::Object,
720 SinkArgKind::Call,
721 SinkArgKind::Other,
722 ] {
723 let encoded = bitcode::encode(&kind);
724 let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
725 assert_eq!(kind, decoded);
726 }
727 }
728
729 #[test]
730 fn sink_site_bitcode_roundtrip() {
731 let site = SinkSite {
732 sink_shape: SinkShape::MemberAssign,
733 callee_path: "el.innerHTML".to_string(),
734 arg_index: 0,
735 arg_is_non_literal: true,
736 arg_kind: SinkArgKind::Other,
737 arg_idents: vec!["userInput".to_string()],
738 span_start: 10,
739 span_end: 20,
740 };
741 let encoded = bitcode::encode(&site);
742 let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
743 assert_eq!(decoded.sink_shape, site.sink_shape);
744 assert_eq!(decoded.callee_path, site.callee_path);
745 assert_eq!(decoded.arg_index, site.arg_index);
746 assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
747 assert_eq!(decoded.arg_kind, site.arg_kind);
748 assert_eq!(decoded.arg_idents, site.arg_idents);
749 assert_eq!(decoded.span(), site.span());
750 }
751
752 #[test]
753 fn line_offsets_single_line_no_newline() {
754 assert_eq!(compute_line_offsets("hello"), vec![0]);
755 }
756
757 #[test]
758 fn line_offsets_single_line_with_newline() {
759 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
760 }
761
762 #[test]
763 fn line_offsets_multiple_lines() {
764 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
765 }
766
767 #[test]
768 fn line_offsets_trailing_newline() {
769 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
770 }
771
772 #[test]
773 fn line_offsets_consecutive_newlines() {
774 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
775 }
776
777 #[test]
778 fn line_offsets_multibyte_utf8() {
779 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
780 }
781
782 #[test]
783 fn line_col_offset_zero() {
784 let offsets = compute_line_offsets("abc\ndef\nghi");
785 let (line, col) = byte_offset_to_line_col(&offsets, 0);
786 assert_eq!((line, col), (1, 0));
787 }
788
789 #[test]
790 fn line_col_middle_of_first_line() {
791 let offsets = compute_line_offsets("abc\ndef\nghi");
792 let (line, col) = byte_offset_to_line_col(&offsets, 2);
793 assert_eq!((line, col), (1, 2));
794 }
795
796 #[test]
797 fn line_col_start_of_second_line() {
798 let offsets = compute_line_offsets("abc\ndef\nghi");
799 let (line, col) = byte_offset_to_line_col(&offsets, 4);
800 assert_eq!((line, col), (2, 0));
801 }
802
803 #[test]
804 fn line_col_middle_of_second_line() {
805 let offsets = compute_line_offsets("abc\ndef\nghi");
806 let (line, col) = byte_offset_to_line_col(&offsets, 5);
807 assert_eq!((line, col), (2, 1));
808 }
809
810 #[test]
811 fn line_col_start_of_third_line() {
812 let offsets = compute_line_offsets("abc\ndef\nghi");
813 let (line, col) = byte_offset_to_line_col(&offsets, 8);
814 assert_eq!((line, col), (3, 0));
815 }
816
817 #[test]
818 fn line_col_end_of_file() {
819 let offsets = compute_line_offsets("abc\ndef\nghi");
820 let (line, col) = byte_offset_to_line_col(&offsets, 10);
821 assert_eq!((line, col), (3, 2));
822 }
823
824 #[test]
825 fn line_col_single_line() {
826 let offsets = compute_line_offsets("hello");
827 let (line, col) = byte_offset_to_line_col(&offsets, 3);
828 assert_eq!((line, col), (1, 3));
829 }
830
831 #[test]
832 fn line_col_at_newline_byte() {
833 let offsets = compute_line_offsets("abc\ndef");
834 let (line, col) = byte_offset_to_line_col(&offsets, 3);
835 assert_eq!((line, col), (1, 3));
836 }
837
838 #[test]
839 fn export_name_matches_str_named() {
840 let name = ExportName::Named("foo".to_string());
841 assert!(name.matches_str("foo"));
842 assert!(!name.matches_str("bar"));
843 assert!(!name.matches_str("default"));
844 }
845
846 #[test]
847 fn export_name_matches_str_default() {
848 let name = ExportName::Default;
849 assert!(name.matches_str("default"));
850 assert!(!name.matches_str("foo"));
851 }
852
853 #[test]
854 fn export_name_display_named() {
855 let name = ExportName::Named("myExport".to_string());
856 assert_eq!(name.to_string(), "myExport");
857 }
858
859 #[test]
860 fn export_name_display_default() {
861 let name = ExportName::Default;
862 assert_eq!(name.to_string(), "default");
863 }
864
865 #[test]
866 fn export_name_equality_named() {
867 let a = ExportName::Named("foo".to_string());
868 let b = ExportName::Named("foo".to_string());
869 let c = ExportName::Named("bar".to_string());
870 assert_eq!(a, b);
871 assert_ne!(a, c);
872 }
873
874 #[test]
875 fn export_name_equality_default() {
876 let a = ExportName::Default;
877 let b = ExportName::Default;
878 assert_eq!(a, b);
879 }
880
881 #[test]
882 fn export_name_named_not_equal_to_default() {
883 let named = ExportName::Named("default".to_string());
884 let default = ExportName::Default;
885 assert_ne!(named, default);
886 }
887
888 #[test]
889 fn export_name_hash_consistency() {
890 use std::collections::hash_map::DefaultHasher;
891 use std::hash::{Hash, Hasher};
892
893 let mut h1 = DefaultHasher::new();
894 let mut h2 = DefaultHasher::new();
895 ExportName::Named("foo".to_string()).hash(&mut h1);
896 ExportName::Named("foo".to_string()).hash(&mut h2);
897 assert_eq!(h1.finish(), h2.finish());
898 }
899
900 #[test]
901 fn export_name_matches_str_empty_string() {
902 let name = ExportName::Named(String::new());
903 assert!(name.matches_str(""));
904 assert!(!name.matches_str("foo"));
905 }
906
907 #[test]
908 fn export_name_default_does_not_match_empty() {
909 let name = ExportName::Default;
910 assert!(!name.matches_str(""));
911 }
912
913 #[test]
914 fn imported_name_equality() {
915 assert_eq!(
916 ImportedName::Named("foo".to_string()),
917 ImportedName::Named("foo".to_string())
918 );
919 assert_ne!(
920 ImportedName::Named("foo".to_string()),
921 ImportedName::Named("bar".to_string())
922 );
923 assert_eq!(ImportedName::Default, ImportedName::Default);
924 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
925 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
926 assert_ne!(ImportedName::Default, ImportedName::Namespace);
927 assert_ne!(
928 ImportedName::Named("default".to_string()),
929 ImportedName::Default
930 );
931 }
932
933 #[test]
934 fn member_kind_equality() {
935 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
936 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
937 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
938 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
939 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
940 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
941 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
942 }
943
944 #[test]
945 fn member_kind_bitcode_roundtrip() {
946 let kinds = [
947 MemberKind::EnumMember,
948 MemberKind::ClassMethod,
949 MemberKind::ClassProperty,
950 MemberKind::NamespaceMember,
951 ];
952 for kind in &kinds {
953 let bytes = bitcode::encode(kind);
954 let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
955 assert_eq!(&decoded, kind);
956 }
957 }
958
959 #[test]
960 fn member_access_bitcode_roundtrip() {
961 let access = MemberAccess {
962 object: "Status".to_string(),
963 member: "Active".to_string(),
964 };
965 let bytes = bitcode::encode(&access);
966 let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
967 assert_eq!(decoded.object, "Status");
968 assert_eq!(decoded.member, "Active");
969 }
970
971 #[test]
972 fn line_offsets_crlf_only_counts_lf() {
973 let offsets = compute_line_offsets("ab\r\ncd");
974 assert_eq!(offsets, vec![0, 4]);
975 }
976
977 #[test]
978 fn line_col_empty_file_offset_zero() {
979 let offsets = compute_line_offsets("");
980 let (line, col) = byte_offset_to_line_col(&offsets, 0);
981 assert_eq!((line, col), (1, 0));
982 }
983
984 #[test]
985 fn function_complexity_bitcode_roundtrip() {
986 let fc = FunctionComplexity {
987 name: "processData".to_string(),
988 line: 42,
989 col: 4,
990 cyclomatic: 15,
991 cognitive: 25,
992 line_count: 80,
993 param_count: 3,
994 source_hash: Some("0123456789abcdef".to_string()),
995 };
996 let bytes = bitcode::encode(&fc);
997 let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
998 assert_eq!(decoded.name, "processData");
999 assert_eq!(decoded.line, 42);
1000 assert_eq!(decoded.col, 4);
1001 assert_eq!(decoded.cyclomatic, 15);
1002 assert_eq!(decoded.cognitive, 25);
1003 assert_eq!(decoded.line_count, 80);
1004 assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
1005 }
1006}