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 package_path_references: Vec<String>,
27 pub member_accesses: Vec<MemberAccess>,
29 pub whole_object_uses: Vec<String>,
31 pub has_cjs_exports: bool,
33 pub has_angular_component_template_url: bool,
35 pub content_hash: u64,
37 pub suppressions: Vec<Suppression>,
39 pub unknown_suppression_kinds: Vec<UnknownSuppressionKind>,
44 pub unused_import_bindings: Vec<String>,
49 pub type_referenced_import_bindings: Vec<String>,
53 pub value_referenced_import_bindings: Vec<String>,
55 pub line_offsets: Vec<u32>,
57 pub complexity: Vec<FunctionComplexity>,
59 pub flag_uses: Vec<FlagUse>,
61 pub class_heritage: Vec<ClassHeritageInfo>,
63 pub injection_tokens: Vec<(String, String)>,
71 pub local_type_declarations: Vec<LocalTypeDeclaration>,
73 pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
75 pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
77 pub iconify_prefixes: Vec<String>,
79 pub iconify_icon_names: Vec<String>,
82 pub auto_import_candidates: Vec<String>,
84 pub directives: Vec<String>,
89 pub security_sinks: Vec<SinkSite>,
93 pub security_sinks_skipped: u32,
98 pub tainted_bindings: Vec<TaintedBinding>,
106 pub sanitized_sink_args: Vec<SanitizedSinkArg>,
110 pub security_control_sites: Vec<SecurityControlSite>,
113}
114
115impl ModuleInfo {
116 pub fn release_resolution_payload(&mut self) {
122 Self::release_vec(&mut self.dynamic_imports);
123 Self::release_vec(&mut self.require_calls);
124 Self::release_vec(&mut self.package_path_references);
125 Self::release_vec(&mut self.whole_object_uses);
126 Self::release_vec(&mut self.unused_import_bindings);
127 Self::release_vec(&mut self.type_referenced_import_bindings);
128 Self::release_vec(&mut self.value_referenced_import_bindings);
129 Self::release_vec(&mut self.namespace_object_aliases);
130 Self::release_vec(&mut self.auto_import_candidates);
131 }
132
133 fn release_vec<T>(values: &mut Vec<T>) {
134 *values = Vec::new();
135 }
136}
137
138#[derive(
140 Debug,
141 Clone,
142 Copy,
143 PartialEq,
144 Eq,
145 PartialOrd,
146 Ord,
147 serde::Serialize,
148 serde::Deserialize,
149 bitcode::Encode,
150 bitcode::Decode,
151)]
152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
153#[serde(rename_all = "kebab-case")]
154pub enum SecurityControlKind {
155 Sanitization,
157 Validation,
159 Authentication,
161 Authorization,
163}
164
165#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
167pub struct SecurityControlSite {
168 pub kind: SecurityControlKind,
170 pub callee_path: String,
173 pub span_start: u32,
175 pub span_end: u32,
177}
178
179#[derive(
182 Debug,
183 Clone,
184 Copy,
185 PartialEq,
186 Eq,
187 serde::Serialize,
188 serde::Deserialize,
189 bitcode::Encode,
190 bitcode::Decode,
191)]
192pub enum SanitizerScope {
193 Html,
195 Url,
197 Path,
199}
200
201#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
203pub struct SanitizedSinkArg {
204 pub span_start: u32,
206 pub arg_index: u32,
208 pub scope: SanitizerScope,
210}
211
212#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
221pub struct TaintedBinding {
222 pub local: String,
224 pub source_path: String,
226 pub source_span_start: u32,
234}
235
236#[derive(
241 Debug,
242 Clone,
243 Copy,
244 PartialEq,
245 Eq,
246 serde::Serialize,
247 serde::Deserialize,
248 bitcode::Encode,
249 bitcode::Decode,
250)]
251pub enum SinkShape {
252 Call,
254 MemberCall,
256 MemberAssign,
258 TaggedTemplate,
260 JsxAttr,
262 NewExpression,
264 SecretLiteral,
267}
268
269#[derive(
276 Debug,
277 Clone,
278 Copy,
279 PartialEq,
280 Eq,
281 serde::Serialize,
282 serde::Deserialize,
283 bitcode::Encode,
284 bitcode::Decode,
285)]
286pub enum SinkArgKind {
287 TemplateWithSubst,
291 Concat,
293 Object,
295 Call,
297 Literal,
299 NoArg,
301 Other,
303}
304
305#[derive(
307 Debug,
308 Clone,
309 PartialEq,
310 Eq,
311 serde::Serialize,
312 serde::Deserialize,
313 bitcode::Encode,
314 bitcode::Decode,
315)]
316pub enum SinkLiteralValue {
317 String(String),
319 Integer(i64),
321 Boolean(bool),
323 Null,
325}
326
327#[derive(
330 Debug,
331 Clone,
332 PartialEq,
333 Eq,
334 serde::Serialize,
335 serde::Deserialize,
336 bitcode::Encode,
337 bitcode::Decode,
338)]
339pub struct SinkObjectProperty {
340 pub key: String,
342 pub value: SinkLiteralValue,
344}
345
346#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
351pub struct SinkSite {
352 pub sink_shape: SinkShape,
354 pub callee_path: String,
356 pub arg_index: u32,
358 pub arg_is_non_literal: bool,
361 pub arg_kind: SinkArgKind,
366 pub arg_literal: Option<SinkLiteralValue>,
368 pub regex_pattern: Option<String>,
370 pub object_properties: Vec<SinkObjectProperty>,
372 pub object_property_keys: Vec<String>,
375 pub object_property_keys_complete: bool,
379 pub arg_idents: Vec<String>,
386 pub arg_source_paths: Vec<String>,
392 pub span_start: u32,
395 pub span_end: u32,
397 pub url_arg_literal: Option<String>,
404}
405
406impl SinkSite {
407 #[must_use]
409 pub fn span(&self) -> Span {
410 Span::new(self.span_start, self.span_end)
411 }
412}
413
414pub const PUBLIC_ENV_PREFIXES: &[&str] = &[
419 "NEXT_PUBLIC_",
420 "VITE_",
421 "NUXT_PUBLIC_",
422 "REACT_APP_",
423 "PUBLIC_",
424 "GATSBY_",
425 "EXPO_PUBLIC_",
426 "STORYBOOK_",
427];
428
429pub const PUBLIC_ENV_EXACT: &[&str] = &["NODE_ENV"];
431
432#[must_use]
435pub fn is_public_env_var(name: &str) -> bool {
436 PUBLIC_ENV_EXACT.contains(&name) || PUBLIC_ENV_PREFIXES.iter().any(|p| name.starts_with(p))
437}
438
439#[must_use]
443pub fn is_public_env_path(path: &str) -> bool {
444 for object in ["process.env.", "import.meta.env."] {
445 if let Some(var) = path.strip_prefix(object) {
446 return is_public_env_var(var);
447 }
448 }
449 false
450}
451
452#[derive(Debug, Clone)]
454pub struct NamespaceObjectAlias {
455 pub via_export_name: String,
457 pub suffix: String,
459 pub namespace_local: String,
461}
462
463#[must_use]
465#[expect(
466 clippy::cast_possible_truncation,
467 reason = "source files are practically < 4GB"
468)]
469pub fn compute_line_offsets(source: &str) -> Vec<u32> {
470 let mut offsets = vec![0u32];
471 for (i, byte) in source.bytes().enumerate() {
472 if byte == b'\n' {
473 debug_assert!(
474 u32::try_from(i + 1).is_ok(),
475 "source file exceeds u32::MAX bytes โ line offsets would overflow"
476 );
477 offsets.push((i + 1) as u32);
478 }
479 }
480 offsets
481}
482
483#[must_use]
485#[expect(
486 clippy::cast_possible_truncation,
487 reason = "line count is bounded by source size"
488)]
489pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
490 let line_idx = match line_offsets.binary_search(&byte_offset) {
491 Ok(idx) => idx,
492 Err(idx) => idx.saturating_sub(1),
493 };
494 let line = line_idx as u32 + 1;
495 let col = byte_offset - line_offsets[line_idx];
496 (line, col)
497}
498
499#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
501pub struct FunctionComplexity {
502 pub name: String,
504 pub line: u32,
506 pub col: u32,
508 pub cyclomatic: u16,
510 pub cognitive: u16,
512 pub line_count: u32,
514 pub param_count: u8,
516 pub source_hash: Option<String>,
518 pub contributions: Vec<ComplexityContribution>,
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
528#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
529#[serde(rename_all = "kebab-case")]
530pub enum ComplexityMetric {
531 Cyclomatic,
533 Cognitive,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
543#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
544#[serde(rename_all = "kebab-case")]
545pub enum ComplexityContributionKind {
546 If,
548 Else,
550 ElseIf,
553 Ternary,
555 LogicalAnd,
557 LogicalOr,
559 NullishCoalescing,
561 LogicalAssignment,
563 OptionalChain,
565 For,
567 ForIn,
569 ForOf,
571 While,
573 DoWhile,
575 Switch,
577 Case,
579 Catch,
581 LabeledBreak,
583 LabeledContinue,
585}
586
587#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
595pub struct ComplexityContribution {
596 pub line: u32,
598 pub col: u32,
600 pub metric: ComplexityMetric,
602 pub kind: ComplexityContributionKind,
604 pub weight: u16,
607 pub nesting: u16,
610}
611
612#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
614pub enum FlagUseKind {
615 EnvVar,
617 SdkCall,
619 ConfigObject,
621}
622
623#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
625pub struct FlagUse {
626 pub flag_name: String,
628 pub kind: FlagUseKind,
630 pub line: u32,
632 pub col: u32,
634 pub guard_span_start: Option<u32>,
636 pub guard_span_end: Option<u32>,
638 pub sdk_name: Option<String>,
640}
641
642const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
643
644#[derive(Debug, Clone)]
646pub struct DynamicImportPattern {
647 pub prefix: String,
649 pub suffix: Option<String>,
651 pub span: Span,
653}
654
655#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
657#[serde(rename_all = "lowercase")]
658#[repr(u8)]
659pub enum VisibilityTag {
660 #[default]
662 None = 0,
663 Public = 1,
665 Internal = 2,
667 Beta = 3,
669 Alpha = 4,
671 ExpectedUnused = 5,
673}
674
675impl VisibilityTag {
676 pub const fn suppresses_unused(self) -> bool {
680 matches!(
681 self,
682 Self::Public | Self::Internal | Self::Beta | Self::Alpha
683 )
684 }
685
686 pub fn is_none(&self) -> bool {
688 matches!(self, Self::None)
689 }
690}
691
692#[derive(Debug, Clone, serde::Serialize)]
694pub struct ExportInfo {
695 pub name: ExportName,
697 pub local_name: Option<String>,
699 pub is_type_only: bool,
701 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
703 pub is_side_effect_used: bool,
704 #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
706 pub visibility: VisibilityTag,
707 #[serde(serialize_with = "serialize_span")]
709 pub span: Span,
710 #[serde(default, skip_serializing_if = "Vec::is_empty")]
712 pub members: Vec<MemberInfo>,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub super_class: Option<String>,
716}
717
718#[derive(
720 Debug,
721 Clone,
722 serde::Serialize,
723 serde::Deserialize,
724 bitcode::Encode,
725 bitcode::Decode,
726 PartialEq,
727 Eq,
728)]
729pub struct ClassHeritageInfo {
730 pub export_name: String,
732 pub super_class: Option<String>,
734 pub implements: Vec<String>,
736 #[serde(default, skip_serializing_if = "Vec::is_empty")]
738 pub instance_bindings: Vec<(String, String)>,
739}
740
741#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
743pub struct LocalTypeDeclaration {
744 pub name: String,
746 #[serde(serialize_with = "serialize_span")]
748 pub span: Span,
749}
750
751#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
753pub struct PublicSignatureTypeReference {
754 pub export_name: String,
756 pub type_name: String,
758 #[serde(serialize_with = "serialize_span")]
760 pub span: Span,
761}
762
763#[derive(Debug, Clone, serde::Serialize)]
765pub struct MemberInfo {
766 pub name: String,
768 pub kind: MemberKind,
770 #[serde(serialize_with = "serialize_span")]
772 pub span: Span,
773 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
778 pub has_decorator: bool,
779 #[serde(default, skip_serializing_if = "Vec::is_empty")]
786 pub decorator_names: Vec<String>,
787 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
794 pub is_instance_returning_static: bool,
795 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
803 pub is_self_returning: bool,
804}
805
806#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
808#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
809#[serde(rename_all = "snake_case")]
810pub enum MemberKind {
811 EnumMember,
813 ClassMethod,
815 ClassProperty,
817 NamespaceMember,
819}
820
821#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
823pub struct MemberAccess {
824 pub object: String,
826 pub member: String,
828}
829
830#[expect(
831 clippy::trivially_copy_pass_by_ref,
832 reason = "serde serialize_with requires &T"
833)]
834fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
835 use serde::ser::SerializeMap;
836 let mut map = serializer.serialize_map(Some(2))?;
837 map.serialize_entry("start", &span.start)?;
838 map.serialize_entry("end", &span.end)?;
839 map.end()
840}
841
842#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
844pub enum ExportName {
845 Named(String),
847 Default,
849}
850
851impl ExportName {
852 #[must_use]
854 pub fn matches_str(&self, s: &str) -> bool {
855 match self {
856 Self::Named(n) => n == s,
857 Self::Default => s == "default",
858 }
859 }
860}
861
862impl std::fmt::Display for ExportName {
863 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
864 match self {
865 Self::Named(n) => write!(f, "{n}"),
866 Self::Default => write!(f, "default"),
867 }
868 }
869}
870
871#[derive(Debug, Clone)]
873pub struct ImportInfo {
874 pub source: String,
876 pub imported_name: ImportedName,
878 pub local_name: String,
880 pub is_type_only: bool,
882 pub from_style: bool,
884 pub span: Span,
886 pub source_span: Span,
888}
889
890#[derive(Debug, Clone, PartialEq, Eq)]
892pub enum ImportedName {
893 Named(String),
895 Default,
897 Namespace,
899 SideEffect,
901}
902
903#[cfg(target_pointer_width = "64")]
904const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
905#[cfg(target_pointer_width = "64")]
906const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
907#[cfg(target_pointer_width = "64")]
908const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
909#[cfg(target_pointer_width = "64")]
910const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
911#[cfg(target_pointer_width = "64")]
912const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
913#[cfg(target_pointer_width = "64")]
914const _: () = assert!(std::mem::size_of::<SinkSite>() == 208);
915#[cfg(target_pointer_width = "64")]
916const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 744);
917
918#[derive(Debug, Clone)]
920pub struct ReExportInfo {
921 pub source: String,
923 pub imported_name: String,
925 pub exported_name: String,
927 pub is_type_only: bool,
929 pub span: oxc_span::Span,
931}
932
933#[derive(Debug, Clone)]
935pub struct DynamicImportInfo {
936 pub source: String,
938 pub span: Span,
940 pub destructured_names: Vec<String>,
944 pub local_name: Option<String>,
947 pub is_speculative: bool,
949}
950
951#[derive(Debug, Clone)]
953pub struct RequireCallInfo {
954 pub source: String,
956 pub span: Span,
958 pub source_span: Span,
964 pub destructured_names: Vec<String>,
966 pub local_name: Option<String>,
968}
969
970pub struct ParseResult {
972 pub modules: Vec<ModuleInfo>,
974 pub cache_hits: usize,
976 pub cache_misses: usize,
978 pub parse_cpu_ms: f64,
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 fn span() -> Span {
987 Span::new(0, 1)
988 }
989
990 macro_rules! assert_released {
991 ($values:expr) => {{
992 assert!($values.is_empty());
993 assert_eq!($values.capacity(), 0);
994 }};
995 }
996
997 #[test]
998 fn line_offsets_empty_string() {
999 assert_eq!(compute_line_offsets(""), vec![0]);
1000 }
1001
1002 #[test]
1003 fn release_resolution_payload_drops_copied_vectors_only() {
1004 let mut module = ModuleInfo {
1005 file_id: FileId(7),
1006 exports: vec![ExportInfo {
1007 name: ExportName::Named("kept".to_string()),
1008 local_name: None,
1009 is_type_only: false,
1010 is_side_effect_used: false,
1011 visibility: VisibilityTag::None,
1012 span: span(),
1013 members: Vec::new(),
1014 super_class: None,
1015 }],
1016 imports: vec![ImportInfo {
1017 source: "node:child_process".to_string(),
1018 imported_name: ImportedName::Default,
1019 local_name: "childProcess".to_string(),
1020 is_type_only: false,
1021 from_style: false,
1022 span: span(),
1023 source_span: span(),
1024 }],
1025 re_exports: vec![ReExportInfo {
1026 source: "./kept".to_string(),
1027 imported_name: "kept".to_string(),
1028 exported_name: "kept".to_string(),
1029 is_type_only: false,
1030 span: span(),
1031 }],
1032 dynamic_imports: vec![DynamicImportInfo {
1033 source: "./dynamic".to_string(),
1034 span: span(),
1035 destructured_names: vec!["value".to_string()],
1036 local_name: None,
1037 is_speculative: false,
1038 }],
1039 dynamic_import_patterns: vec![DynamicImportPattern {
1040 prefix: "./pages/".to_string(),
1041 suffix: Some(".tsx".to_string()),
1042 span: span(),
1043 }],
1044 require_calls: vec![RequireCallInfo {
1045 source: "./required".to_string(),
1046 span: span(),
1047 source_span: span(),
1048 destructured_names: Vec::new(),
1049 local_name: Some("required".to_string()),
1050 }],
1051 package_path_references: vec!["react".to_string()],
1052 member_accesses: vec![MemberAccess {
1053 object: "Status".to_string(),
1054 member: "Active".to_string(),
1055 }],
1056 whole_object_uses: vec!["Status".to_string()],
1057 has_cjs_exports: true,
1058 has_angular_component_template_url: true,
1059 content_hash: 42,
1060 suppressions: Vec::new(),
1061 unknown_suppression_kinds: Vec::new(),
1062 unused_import_bindings: vec!["unused".to_string()],
1063 type_referenced_import_bindings: vec!["TypeOnly".to_string()],
1064 value_referenced_import_bindings: vec!["Value".to_string()],
1065 line_offsets: vec![0, 8],
1066 complexity: vec![FunctionComplexity {
1067 name: "work".to_string(),
1068 line: 1,
1069 col: 0,
1070 cyclomatic: 2,
1071 cognitive: 3,
1072 line_count: 4,
1073 param_count: 1,
1074 source_hash: Some("hash".to_string()),
1075 contributions: Vec::new(),
1076 }],
1077 flag_uses: vec![FlagUse {
1078 flag_name: "FEATURE_X".to_string(),
1079 kind: FlagUseKind::EnvVar,
1080 line: 1,
1081 col: 0,
1082 guard_span_start: None,
1083 guard_span_end: None,
1084 sdk_name: None,
1085 }],
1086 class_heritage: vec![ClassHeritageInfo {
1087 export_name: "Child".to_string(),
1088 super_class: Some("Parent".to_string()),
1089 implements: vec!["Contract".to_string()],
1090 instance_bindings: Vec::new(),
1091 }],
1092 injection_tokens: vec![("TOKEN".to_string(), "Contract".to_string())],
1093 local_type_declarations: vec![LocalTypeDeclaration {
1094 name: "Contract".to_string(),
1095 span: span(),
1096 }],
1097 public_signature_type_references: vec![PublicSignatureTypeReference {
1098 export_name: "kept".to_string(),
1099 type_name: "Contract".to_string(),
1100 span: span(),
1101 }],
1102 namespace_object_aliases: vec![NamespaceObjectAlias {
1103 via_export_name: "api".to_string(),
1104 suffix: "read".to_string(),
1105 namespace_local: "ns".to_string(),
1106 }],
1107 iconify_prefixes: vec!["hero".to_string()],
1108 iconify_icon_names: vec!["hero-home".to_string()],
1109 auto_import_candidates: vec!["useState".to_string()],
1110 directives: vec!["use client".to_string()],
1111 security_sinks: Vec::new(),
1112 security_sinks_skipped: 1,
1113 tainted_bindings: Vec::new(),
1114 sanitized_sink_args: Vec::new(),
1115 security_control_sites: Vec::new(),
1116 };
1117
1118 module.release_resolution_payload();
1119
1120 assert_eq!(module.file_id, FileId(7));
1121 assert_eq!(module.content_hash, 42);
1122 assert_eq!(module.line_offsets, vec![0, 8]);
1123 assert_eq!(module.imports.len(), 1);
1124 assert_eq!(module.exports.len(), 1);
1125 assert_eq!(module.re_exports.len(), 1);
1126 assert_eq!(module.dynamic_import_patterns.len(), 1);
1127 assert_eq!(module.member_accesses.len(), 1);
1128 assert_eq!(module.complexity.len(), 1);
1129 assert_eq!(module.flag_uses.len(), 1);
1130 assert_eq!(module.class_heritage.len(), 1);
1131 assert_eq!(module.injection_tokens.len(), 1);
1132 assert_eq!(module.local_type_declarations.len(), 1);
1133 assert_eq!(module.public_signature_type_references.len(), 1);
1134 assert_eq!(module.iconify_prefixes.len(), 1);
1135 assert_eq!(module.iconify_icon_names.len(), 1);
1136 assert_eq!(module.directives.len(), 1);
1137 assert_eq!(module.security_sinks_skipped, 1);
1138 assert_released!(module.dynamic_imports);
1139 assert_released!(module.require_calls);
1140 assert_released!(module.package_path_references);
1141 assert_released!(module.whole_object_uses);
1142 assert_released!(module.unused_import_bindings);
1143 assert_released!(module.type_referenced_import_bindings);
1144 assert_released!(module.value_referenced_import_bindings);
1145 assert_released!(module.namespace_object_aliases);
1146 assert_released!(module.auto_import_candidates);
1147 }
1148
1149 #[test]
1150 fn sink_shape_bitcode_roundtrip() {
1151 for shape in [
1152 SinkShape::Call,
1153 SinkShape::MemberCall,
1154 SinkShape::MemberAssign,
1155 SinkShape::TaggedTemplate,
1156 SinkShape::JsxAttr,
1157 SinkShape::NewExpression,
1158 SinkShape::SecretLiteral,
1159 ] {
1160 let encoded = bitcode::encode(&shape);
1161 let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
1162 assert_eq!(shape, decoded);
1163 }
1164 }
1165
1166 #[test]
1167 fn sink_arg_kind_bitcode_roundtrip() {
1168 for kind in [
1169 SinkArgKind::TemplateWithSubst,
1170 SinkArgKind::Concat,
1171 SinkArgKind::Object,
1172 SinkArgKind::Call,
1173 SinkArgKind::Literal,
1174 SinkArgKind::NoArg,
1175 SinkArgKind::Other,
1176 ] {
1177 let encoded = bitcode::encode(&kind);
1178 let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
1179 assert_eq!(kind, decoded);
1180 }
1181 }
1182
1183 #[test]
1184 fn sink_site_bitcode_roundtrip() {
1185 let site = SinkSite {
1186 sink_shape: SinkShape::MemberAssign,
1187 callee_path: "el.innerHTML".to_string(),
1188 arg_index: 0,
1189 arg_is_non_literal: true,
1190 arg_kind: SinkArgKind::Other,
1191 arg_literal: Some(SinkLiteralValue::Integer(511)),
1192 regex_pattern: None,
1193 object_properties: vec![SinkObjectProperty {
1194 key: "origin".to_string(),
1195 value: SinkLiteralValue::String("*".to_string()),
1196 }],
1197 object_property_keys: vec!["origin".to_string()],
1198 object_property_keys_complete: true,
1199 arg_idents: vec!["userInput".to_string()],
1200 arg_source_paths: vec!["req.body.email".to_string(), "req.body".to_string()],
1201 span_start: 10,
1202 span_end: 20,
1203 url_arg_literal: Some("https://api.example.com".to_string()),
1204 };
1205 let encoded = bitcode::encode(&site);
1206 let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
1207 assert_eq!(decoded.sink_shape, site.sink_shape);
1208 assert_eq!(decoded.callee_path, site.callee_path);
1209 assert_eq!(decoded.arg_index, site.arg_index);
1210 assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
1211 assert_eq!(decoded.arg_kind, site.arg_kind);
1212 assert_eq!(decoded.arg_literal, site.arg_literal);
1213 assert_eq!(decoded.object_properties, site.object_properties);
1214 assert_eq!(decoded.object_property_keys, site.object_property_keys);
1215 assert_eq!(
1216 decoded.object_property_keys_complete,
1217 site.object_property_keys_complete
1218 );
1219 assert_eq!(decoded.arg_idents, site.arg_idents);
1220 assert_eq!(decoded.arg_source_paths, site.arg_source_paths);
1221 assert_eq!(decoded.span(), site.span());
1222 }
1223
1224 #[test]
1225 fn line_offsets_single_line_no_newline() {
1226 assert_eq!(compute_line_offsets("hello"), vec![0]);
1227 }
1228
1229 #[test]
1230 fn line_offsets_single_line_with_newline() {
1231 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
1232 }
1233
1234 #[test]
1235 fn line_offsets_multiple_lines() {
1236 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
1237 }
1238
1239 #[test]
1240 fn line_offsets_trailing_newline() {
1241 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
1242 }
1243
1244 #[test]
1245 fn line_offsets_consecutive_newlines() {
1246 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
1247 }
1248
1249 #[test]
1250 fn line_offsets_multibyte_utf8() {
1251 assert_eq!(compute_line_offsets("รก\n"), vec![0, 3]);
1252 }
1253
1254 #[test]
1255 fn line_col_offset_zero() {
1256 let offsets = compute_line_offsets("abc\ndef\nghi");
1257 let (line, col) = byte_offset_to_line_col(&offsets, 0);
1258 assert_eq!((line, col), (1, 0));
1259 }
1260
1261 #[test]
1262 fn line_col_middle_of_first_line() {
1263 let offsets = compute_line_offsets("abc\ndef\nghi");
1264 let (line, col) = byte_offset_to_line_col(&offsets, 2);
1265 assert_eq!((line, col), (1, 2));
1266 }
1267
1268 #[test]
1269 fn line_col_start_of_second_line() {
1270 let offsets = compute_line_offsets("abc\ndef\nghi");
1271 let (line, col) = byte_offset_to_line_col(&offsets, 4);
1272 assert_eq!((line, col), (2, 0));
1273 }
1274
1275 #[test]
1276 fn line_col_middle_of_second_line() {
1277 let offsets = compute_line_offsets("abc\ndef\nghi");
1278 let (line, col) = byte_offset_to_line_col(&offsets, 5);
1279 assert_eq!((line, col), (2, 1));
1280 }
1281
1282 #[test]
1283 fn line_col_start_of_third_line() {
1284 let offsets = compute_line_offsets("abc\ndef\nghi");
1285 let (line, col) = byte_offset_to_line_col(&offsets, 8);
1286 assert_eq!((line, col), (3, 0));
1287 }
1288
1289 #[test]
1290 fn line_col_end_of_file() {
1291 let offsets = compute_line_offsets("abc\ndef\nghi");
1292 let (line, col) = byte_offset_to_line_col(&offsets, 10);
1293 assert_eq!((line, col), (3, 2));
1294 }
1295
1296 #[test]
1297 fn line_col_single_line() {
1298 let offsets = compute_line_offsets("hello");
1299 let (line, col) = byte_offset_to_line_col(&offsets, 3);
1300 assert_eq!((line, col), (1, 3));
1301 }
1302
1303 #[test]
1304 fn line_col_at_newline_byte() {
1305 let offsets = compute_line_offsets("abc\ndef");
1306 let (line, col) = byte_offset_to_line_col(&offsets, 3);
1307 assert_eq!((line, col), (1, 3));
1308 }
1309
1310 #[test]
1311 fn export_name_matches_str_named() {
1312 let name = ExportName::Named("foo".to_string());
1313 assert!(name.matches_str("foo"));
1314 assert!(!name.matches_str("bar"));
1315 assert!(!name.matches_str("default"));
1316 }
1317
1318 #[test]
1319 fn export_name_matches_str_default() {
1320 let name = ExportName::Default;
1321 assert!(name.matches_str("default"));
1322 assert!(!name.matches_str("foo"));
1323 }
1324
1325 #[test]
1326 fn export_name_display_named() {
1327 let name = ExportName::Named("myExport".to_string());
1328 assert_eq!(name.to_string(), "myExport");
1329 }
1330
1331 #[test]
1332 fn export_name_display_default() {
1333 let name = ExportName::Default;
1334 assert_eq!(name.to_string(), "default");
1335 }
1336
1337 #[test]
1338 fn export_name_equality_named() {
1339 let a = ExportName::Named("foo".to_string());
1340 let b = ExportName::Named("foo".to_string());
1341 let c = ExportName::Named("bar".to_string());
1342 assert_eq!(a, b);
1343 assert_ne!(a, c);
1344 }
1345
1346 #[test]
1347 fn export_name_equality_default() {
1348 let a = ExportName::Default;
1349 let b = ExportName::Default;
1350 assert_eq!(a, b);
1351 }
1352
1353 #[test]
1354 fn export_name_named_not_equal_to_default() {
1355 let named = ExportName::Named("default".to_string());
1356 let default = ExportName::Default;
1357 assert_ne!(named, default);
1358 }
1359
1360 #[test]
1361 fn export_name_hash_consistency() {
1362 use std::collections::hash_map::DefaultHasher;
1363 use std::hash::{Hash, Hasher};
1364
1365 let mut h1 = DefaultHasher::new();
1366 let mut h2 = DefaultHasher::new();
1367 ExportName::Named("foo".to_string()).hash(&mut h1);
1368 ExportName::Named("foo".to_string()).hash(&mut h2);
1369 assert_eq!(h1.finish(), h2.finish());
1370 }
1371
1372 #[test]
1373 fn export_name_matches_str_empty_string() {
1374 let name = ExportName::Named(String::new());
1375 assert!(name.matches_str(""));
1376 assert!(!name.matches_str("foo"));
1377 }
1378
1379 #[test]
1380 fn export_name_default_does_not_match_empty() {
1381 let name = ExportName::Default;
1382 assert!(!name.matches_str(""));
1383 }
1384
1385 #[test]
1386 fn imported_name_equality() {
1387 assert_eq!(
1388 ImportedName::Named("foo".to_string()),
1389 ImportedName::Named("foo".to_string())
1390 );
1391 assert_ne!(
1392 ImportedName::Named("foo".to_string()),
1393 ImportedName::Named("bar".to_string())
1394 );
1395 assert_eq!(ImportedName::Default, ImportedName::Default);
1396 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
1397 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
1398 assert_ne!(ImportedName::Default, ImportedName::Namespace);
1399 assert_ne!(
1400 ImportedName::Named("default".to_string()),
1401 ImportedName::Default
1402 );
1403 }
1404
1405 #[test]
1406 fn member_kind_equality() {
1407 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
1408 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
1409 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
1410 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
1411 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
1412 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
1413 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
1414 }
1415
1416 #[test]
1417 fn member_kind_bitcode_roundtrip() {
1418 let kinds = [
1419 MemberKind::EnumMember,
1420 MemberKind::ClassMethod,
1421 MemberKind::ClassProperty,
1422 MemberKind::NamespaceMember,
1423 ];
1424 for kind in &kinds {
1425 let bytes = bitcode::encode(kind);
1426 let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
1427 assert_eq!(&decoded, kind);
1428 }
1429 }
1430
1431 #[test]
1432 fn member_access_bitcode_roundtrip() {
1433 let access = MemberAccess {
1434 object: "Status".to_string(),
1435 member: "Active".to_string(),
1436 };
1437 let bytes = bitcode::encode(&access);
1438 let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
1439 assert_eq!(decoded.object, "Status");
1440 assert_eq!(decoded.member, "Active");
1441 }
1442
1443 #[test]
1444 fn line_offsets_crlf_only_counts_lf() {
1445 let offsets = compute_line_offsets("ab\r\ncd");
1446 assert_eq!(offsets, vec![0, 4]);
1447 }
1448
1449 #[test]
1450 fn line_col_empty_file_offset_zero() {
1451 let offsets = compute_line_offsets("");
1452 let (line, col) = byte_offset_to_line_col(&offsets, 0);
1453 assert_eq!((line, col), (1, 0));
1454 }
1455
1456 #[test]
1457 fn function_complexity_bitcode_roundtrip() {
1458 let fc = FunctionComplexity {
1459 name: "processData".to_string(),
1460 line: 42,
1461 col: 4,
1462 cyclomatic: 15,
1463 cognitive: 25,
1464 line_count: 80,
1465 param_count: 3,
1466 source_hash: Some("0123456789abcdef".to_string()),
1467 contributions: vec![
1468 ComplexityContribution {
1469 line: 43,
1470 col: 8,
1471 metric: ComplexityMetric::Cyclomatic,
1472 kind: ComplexityContributionKind::If,
1473 weight: 1,
1474 nesting: 0,
1475 },
1476 ComplexityContribution {
1477 line: 45,
1478 col: 12,
1479 metric: ComplexityMetric::Cognitive,
1480 kind: ComplexityContributionKind::ElseIf,
1481 weight: 3,
1482 nesting: 2,
1483 },
1484 ],
1485 };
1486 let bytes = bitcode::encode(&fc);
1487 let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
1488 assert_eq!(decoded.name, "processData");
1489 assert_eq!(decoded.line, 42);
1490 assert_eq!(decoded.col, 4);
1491 assert_eq!(decoded.cyclomatic, 15);
1492 assert_eq!(decoded.cognitive, 25);
1493 assert_eq!(decoded.line_count, 80);
1494 assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
1495 assert_eq!(decoded.contributions.len(), 2);
1496 assert_eq!(
1497 decoded.contributions[1].kind,
1498 ComplexityContributionKind::ElseIf
1499 );
1500 assert_eq!(decoded.contributions[1].weight, 3);
1501 assert_eq!(decoded.contributions[1].nesting, 2);
1502 assert_eq!(decoded.contributions[1].metric, ComplexityMetric::Cognitive);
1503 }
1504}