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}
86
87#[derive(
92 Debug,
93 Clone,
94 Copy,
95 PartialEq,
96 Eq,
97 serde::Serialize,
98 serde::Deserialize,
99 bitcode::Encode,
100 bitcode::Decode,
101)]
102pub enum SinkShape {
103 Call,
105 MemberCall,
107 MemberAssign,
109 TaggedTemplate,
111 JsxAttr,
113}
114
115#[derive(
122 Debug,
123 Clone,
124 Copy,
125 PartialEq,
126 Eq,
127 serde::Serialize,
128 serde::Deserialize,
129 bitcode::Encode,
130 bitcode::Decode,
131)]
132pub enum SinkArgKind {
133 TemplateWithSubst,
137 Concat,
139 Object,
141 Call,
143 Other,
145}
146
147#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
152pub struct SinkSite {
153 pub sink_shape: SinkShape,
155 pub callee_path: String,
157 pub arg_index: u32,
159 pub arg_is_non_literal: bool,
161 pub arg_kind: SinkArgKind,
166 pub span_start: u32,
169 pub span_end: u32,
171}
172
173impl SinkSite {
174 #[must_use]
176 pub fn span(&self) -> Span {
177 Span::new(self.span_start, self.span_end)
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct NamespaceObjectAlias {
184 pub via_export_name: String,
186 pub suffix: String,
188 pub namespace_local: String,
190}
191
192#[must_use]
194#[expect(
195 clippy::cast_possible_truncation,
196 reason = "source files are practically < 4GB"
197)]
198pub fn compute_line_offsets(source: &str) -> Vec<u32> {
199 let mut offsets = vec![0u32];
200 for (i, byte) in source.bytes().enumerate() {
201 if byte == b'\n' {
202 debug_assert!(
203 u32::try_from(i + 1).is_ok(),
204 "source file exceeds u32::MAX bytes — line offsets would overflow"
205 );
206 offsets.push((i + 1) as u32);
207 }
208 }
209 offsets
210}
211
212#[must_use]
214#[expect(
215 clippy::cast_possible_truncation,
216 reason = "line count is bounded by source size"
217)]
218pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
219 let line_idx = match line_offsets.binary_search(&byte_offset) {
220 Ok(idx) => idx,
221 Err(idx) => idx.saturating_sub(1),
222 };
223 let line = line_idx as u32 + 1;
224 let col = byte_offset - line_offsets[line_idx];
225 (line, col)
226}
227
228#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
230pub struct FunctionComplexity {
231 pub name: String,
233 pub line: u32,
235 pub col: u32,
237 pub cyclomatic: u16,
239 pub cognitive: u16,
241 pub line_count: u32,
243 pub param_count: u8,
245 pub source_hash: Option<String>,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
251pub enum FlagUseKind {
252 EnvVar,
254 SdkCall,
256 ConfigObject,
258}
259
260#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
262pub struct FlagUse {
263 pub flag_name: String,
265 pub kind: FlagUseKind,
267 pub line: u32,
269 pub col: u32,
271 pub guard_span_start: Option<u32>,
273 pub guard_span_end: Option<u32>,
275 pub sdk_name: Option<String>,
277}
278
279const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
280
281#[derive(Debug, Clone)]
283pub struct DynamicImportPattern {
284 pub prefix: String,
286 pub suffix: Option<String>,
288 pub span: Span,
290}
291
292#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
294#[serde(rename_all = "lowercase")]
295#[repr(u8)]
296pub enum VisibilityTag {
297 #[default]
299 None = 0,
300 Public = 1,
302 Internal = 2,
304 Beta = 3,
306 Alpha = 4,
308 ExpectedUnused = 5,
310}
311
312impl VisibilityTag {
313 pub const fn suppresses_unused(self) -> bool {
317 matches!(
318 self,
319 Self::Public | Self::Internal | Self::Beta | Self::Alpha
320 )
321 }
322
323 pub fn is_none(&self) -> bool {
325 matches!(self, Self::None)
326 }
327}
328
329#[derive(Debug, Clone, serde::Serialize)]
331pub struct ExportInfo {
332 pub name: ExportName,
334 pub local_name: Option<String>,
336 pub is_type_only: bool,
338 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
340 pub is_side_effect_used: bool,
341 #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
343 pub visibility: VisibilityTag,
344 #[serde(serialize_with = "serialize_span")]
346 pub span: Span,
347 #[serde(default, skip_serializing_if = "Vec::is_empty")]
349 pub members: Vec<MemberInfo>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub super_class: Option<String>,
353}
354
355#[derive(
357 Debug,
358 Clone,
359 serde::Serialize,
360 serde::Deserialize,
361 bitcode::Encode,
362 bitcode::Decode,
363 PartialEq,
364 Eq,
365)]
366pub struct ClassHeritageInfo {
367 pub export_name: String,
369 pub super_class: Option<String>,
371 pub implements: Vec<String>,
373 #[serde(default, skip_serializing_if = "Vec::is_empty")]
375 pub instance_bindings: Vec<(String, String)>,
376}
377
378#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
380pub struct LocalTypeDeclaration {
381 pub name: String,
383 #[serde(serialize_with = "serialize_span")]
385 pub span: Span,
386}
387
388#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
390pub struct PublicSignatureTypeReference {
391 pub export_name: String,
393 pub type_name: String,
395 #[serde(serialize_with = "serialize_span")]
397 pub span: Span,
398}
399
400#[derive(Debug, Clone, serde::Serialize)]
402pub struct MemberInfo {
403 pub name: String,
405 pub kind: MemberKind,
407 #[serde(serialize_with = "serialize_span")]
409 pub span: Span,
410 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
415 pub has_decorator: bool,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
423 pub decorator_names: Vec<String>,
424 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
431 pub is_instance_returning_static: bool,
432 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
440 pub is_self_returning: bool,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
445#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
446#[serde(rename_all = "snake_case")]
447pub enum MemberKind {
448 EnumMember,
450 ClassMethod,
452 ClassProperty,
454 NamespaceMember,
456}
457
458#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
460pub struct MemberAccess {
461 pub object: String,
463 pub member: String,
465}
466
467#[expect(
468 clippy::trivially_copy_pass_by_ref,
469 reason = "serde serialize_with requires &T"
470)]
471fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
472 use serde::ser::SerializeMap;
473 let mut map = serializer.serialize_map(Some(2))?;
474 map.serialize_entry("start", &span.start)?;
475 map.serialize_entry("end", &span.end)?;
476 map.end()
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
481pub enum ExportName {
482 Named(String),
484 Default,
486}
487
488impl ExportName {
489 #[must_use]
491 pub fn matches_str(&self, s: &str) -> bool {
492 match self {
493 Self::Named(n) => n == s,
494 Self::Default => s == "default",
495 }
496 }
497}
498
499impl std::fmt::Display for ExportName {
500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501 match self {
502 Self::Named(n) => write!(f, "{n}"),
503 Self::Default => write!(f, "default"),
504 }
505 }
506}
507
508#[derive(Debug, Clone)]
510pub struct ImportInfo {
511 pub source: String,
513 pub imported_name: ImportedName,
515 pub local_name: String,
517 pub is_type_only: bool,
519 pub from_style: bool,
521 pub span: Span,
523 pub source_span: Span,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq)]
529pub enum ImportedName {
530 Named(String),
532 Default,
534 Namespace,
536 SideEffect,
538}
539
540#[cfg(target_pointer_width = "64")]
541const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
542#[cfg(target_pointer_width = "64")]
543const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
544#[cfg(target_pointer_width = "64")]
545const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
546#[cfg(target_pointer_width = "64")]
547const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
548#[cfg(target_pointer_width = "64")]
549const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
550#[cfg(target_pointer_width = "64")]
551const _: () = assert!(std::mem::size_of::<SinkSite>() == 40);
552#[cfg(target_pointer_width = "64")]
553const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 600);
554
555#[derive(Debug, Clone)]
557pub struct ReExportInfo {
558 pub source: String,
560 pub imported_name: String,
562 pub exported_name: String,
564 pub is_type_only: bool,
566 pub span: oxc_span::Span,
568}
569
570#[derive(Debug, Clone)]
572pub struct DynamicImportInfo {
573 pub source: String,
575 pub span: Span,
577 pub destructured_names: Vec<String>,
581 pub local_name: Option<String>,
584 pub is_speculative: bool,
586}
587
588#[derive(Debug, Clone)]
590pub struct RequireCallInfo {
591 pub source: String,
593 pub span: Span,
595 pub destructured_names: Vec<String>,
597 pub local_name: Option<String>,
599}
600
601pub struct ParseResult {
603 pub modules: Vec<ModuleInfo>,
605 pub cache_hits: usize,
607 pub cache_misses: usize,
609 pub parse_cpu_ms: f64,
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn line_offsets_empty_string() {
619 assert_eq!(compute_line_offsets(""), vec![0]);
620 }
621
622 #[test]
623 fn sink_shape_bitcode_roundtrip() {
624 for shape in [
625 SinkShape::Call,
626 SinkShape::MemberCall,
627 SinkShape::MemberAssign,
628 SinkShape::TaggedTemplate,
629 SinkShape::JsxAttr,
630 ] {
631 let encoded = bitcode::encode(&shape);
632 let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
633 assert_eq!(shape, decoded);
634 }
635 }
636
637 #[test]
638 fn sink_arg_kind_bitcode_roundtrip() {
639 for kind in [
640 SinkArgKind::TemplateWithSubst,
641 SinkArgKind::Concat,
642 SinkArgKind::Object,
643 SinkArgKind::Call,
644 SinkArgKind::Other,
645 ] {
646 let encoded = bitcode::encode(&kind);
647 let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
648 assert_eq!(kind, decoded);
649 }
650 }
651
652 #[test]
653 fn sink_site_bitcode_roundtrip() {
654 let site = SinkSite {
655 sink_shape: SinkShape::MemberAssign,
656 callee_path: "el.innerHTML".to_string(),
657 arg_index: 0,
658 arg_is_non_literal: true,
659 arg_kind: SinkArgKind::Other,
660 span_start: 10,
661 span_end: 20,
662 };
663 let encoded = bitcode::encode(&site);
664 let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
665 assert_eq!(decoded.sink_shape, site.sink_shape);
666 assert_eq!(decoded.callee_path, site.callee_path);
667 assert_eq!(decoded.arg_index, site.arg_index);
668 assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
669 assert_eq!(decoded.arg_kind, site.arg_kind);
670 assert_eq!(decoded.span(), site.span());
671 }
672
673 #[test]
674 fn line_offsets_single_line_no_newline() {
675 assert_eq!(compute_line_offsets("hello"), vec![0]);
676 }
677
678 #[test]
679 fn line_offsets_single_line_with_newline() {
680 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
681 }
682
683 #[test]
684 fn line_offsets_multiple_lines() {
685 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
686 }
687
688 #[test]
689 fn line_offsets_trailing_newline() {
690 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
691 }
692
693 #[test]
694 fn line_offsets_consecutive_newlines() {
695 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
696 }
697
698 #[test]
699 fn line_offsets_multibyte_utf8() {
700 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
701 }
702
703 #[test]
704 fn line_col_offset_zero() {
705 let offsets = compute_line_offsets("abc\ndef\nghi");
706 let (line, col) = byte_offset_to_line_col(&offsets, 0);
707 assert_eq!((line, col), (1, 0));
708 }
709
710 #[test]
711 fn line_col_middle_of_first_line() {
712 let offsets = compute_line_offsets("abc\ndef\nghi");
713 let (line, col) = byte_offset_to_line_col(&offsets, 2);
714 assert_eq!((line, col), (1, 2));
715 }
716
717 #[test]
718 fn line_col_start_of_second_line() {
719 let offsets = compute_line_offsets("abc\ndef\nghi");
720 let (line, col) = byte_offset_to_line_col(&offsets, 4);
721 assert_eq!((line, col), (2, 0));
722 }
723
724 #[test]
725 fn line_col_middle_of_second_line() {
726 let offsets = compute_line_offsets("abc\ndef\nghi");
727 let (line, col) = byte_offset_to_line_col(&offsets, 5);
728 assert_eq!((line, col), (2, 1));
729 }
730
731 #[test]
732 fn line_col_start_of_third_line() {
733 let offsets = compute_line_offsets("abc\ndef\nghi");
734 let (line, col) = byte_offset_to_line_col(&offsets, 8);
735 assert_eq!((line, col), (3, 0));
736 }
737
738 #[test]
739 fn line_col_end_of_file() {
740 let offsets = compute_line_offsets("abc\ndef\nghi");
741 let (line, col) = byte_offset_to_line_col(&offsets, 10);
742 assert_eq!((line, col), (3, 2));
743 }
744
745 #[test]
746 fn line_col_single_line() {
747 let offsets = compute_line_offsets("hello");
748 let (line, col) = byte_offset_to_line_col(&offsets, 3);
749 assert_eq!((line, col), (1, 3));
750 }
751
752 #[test]
753 fn line_col_at_newline_byte() {
754 let offsets = compute_line_offsets("abc\ndef");
755 let (line, col) = byte_offset_to_line_col(&offsets, 3);
756 assert_eq!((line, col), (1, 3));
757 }
758
759 #[test]
760 fn export_name_matches_str_named() {
761 let name = ExportName::Named("foo".to_string());
762 assert!(name.matches_str("foo"));
763 assert!(!name.matches_str("bar"));
764 assert!(!name.matches_str("default"));
765 }
766
767 #[test]
768 fn export_name_matches_str_default() {
769 let name = ExportName::Default;
770 assert!(name.matches_str("default"));
771 assert!(!name.matches_str("foo"));
772 }
773
774 #[test]
775 fn export_name_display_named() {
776 let name = ExportName::Named("myExport".to_string());
777 assert_eq!(name.to_string(), "myExport");
778 }
779
780 #[test]
781 fn export_name_display_default() {
782 let name = ExportName::Default;
783 assert_eq!(name.to_string(), "default");
784 }
785
786 #[test]
787 fn export_name_equality_named() {
788 let a = ExportName::Named("foo".to_string());
789 let b = ExportName::Named("foo".to_string());
790 let c = ExportName::Named("bar".to_string());
791 assert_eq!(a, b);
792 assert_ne!(a, c);
793 }
794
795 #[test]
796 fn export_name_equality_default() {
797 let a = ExportName::Default;
798 let b = ExportName::Default;
799 assert_eq!(a, b);
800 }
801
802 #[test]
803 fn export_name_named_not_equal_to_default() {
804 let named = ExportName::Named("default".to_string());
805 let default = ExportName::Default;
806 assert_ne!(named, default);
807 }
808
809 #[test]
810 fn export_name_hash_consistency() {
811 use std::collections::hash_map::DefaultHasher;
812 use std::hash::{Hash, Hasher};
813
814 let mut h1 = DefaultHasher::new();
815 let mut h2 = DefaultHasher::new();
816 ExportName::Named("foo".to_string()).hash(&mut h1);
817 ExportName::Named("foo".to_string()).hash(&mut h2);
818 assert_eq!(h1.finish(), h2.finish());
819 }
820
821 #[test]
822 fn export_name_matches_str_empty_string() {
823 let name = ExportName::Named(String::new());
824 assert!(name.matches_str(""));
825 assert!(!name.matches_str("foo"));
826 }
827
828 #[test]
829 fn export_name_default_does_not_match_empty() {
830 let name = ExportName::Default;
831 assert!(!name.matches_str(""));
832 }
833
834 #[test]
835 fn imported_name_equality() {
836 assert_eq!(
837 ImportedName::Named("foo".to_string()),
838 ImportedName::Named("foo".to_string())
839 );
840 assert_ne!(
841 ImportedName::Named("foo".to_string()),
842 ImportedName::Named("bar".to_string())
843 );
844 assert_eq!(ImportedName::Default, ImportedName::Default);
845 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
846 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
847 assert_ne!(ImportedName::Default, ImportedName::Namespace);
848 assert_ne!(
849 ImportedName::Named("default".to_string()),
850 ImportedName::Default
851 );
852 }
853
854 #[test]
855 fn member_kind_equality() {
856 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
857 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
858 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
859 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
860 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
861 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
862 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
863 }
864
865 #[test]
866 fn member_kind_bitcode_roundtrip() {
867 let kinds = [
868 MemberKind::EnumMember,
869 MemberKind::ClassMethod,
870 MemberKind::ClassProperty,
871 MemberKind::NamespaceMember,
872 ];
873 for kind in &kinds {
874 let bytes = bitcode::encode(kind);
875 let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
876 assert_eq!(&decoded, kind);
877 }
878 }
879
880 #[test]
881 fn member_access_bitcode_roundtrip() {
882 let access = MemberAccess {
883 object: "Status".to_string(),
884 member: "Active".to_string(),
885 };
886 let bytes = bitcode::encode(&access);
887 let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
888 assert_eq!(decoded.object, "Status");
889 assert_eq!(decoded.member, "Active");
890 }
891
892 #[test]
893 fn line_offsets_crlf_only_counts_lf() {
894 let offsets = compute_line_offsets("ab\r\ncd");
895 assert_eq!(offsets, vec![0, 4]);
896 }
897
898 #[test]
899 fn line_col_empty_file_offset_zero() {
900 let offsets = compute_line_offsets("");
901 let (line, col) = byte_offset_to_line_col(&offsets, 0);
902 assert_eq!((line, col), (1, 0));
903 }
904
905 #[test]
906 fn function_complexity_bitcode_roundtrip() {
907 let fc = FunctionComplexity {
908 name: "processData".to_string(),
909 line: 42,
910 col: 4,
911 cyclomatic: 15,
912 cognitive: 25,
913 line_count: 80,
914 param_count: 3,
915 source_hash: Some("0123456789abcdef".to_string()),
916 };
917 let bytes = bitcode::encode(&fc);
918 let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
919 assert_eq!(decoded.name, "processData");
920 assert_eq!(decoded.line, 42);
921 assert_eq!(decoded.col, 4);
922 assert_eq!(decoded.cyclomatic, 15);
923 assert_eq!(decoded.cognitive, 25);
924 assert_eq!(decoded.line_count, 80);
925 assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
926 }
927}