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 security_unresolved_callee_sites: Vec<SkippedSecurityCalleeSite>,
103 pub tainted_bindings: Vec<TaintedBinding>,
111 pub sanitized_sink_args: Vec<SanitizedSinkArg>,
115 pub security_control_sites: Vec<SecurityControlSite>,
118 pub callee_uses: Vec<CalleeUse>,
124}
125
126impl ModuleInfo {
127 pub fn release_resolution_payload(&mut self) {
133 Self::release_vec(&mut self.dynamic_imports);
134 Self::release_vec(&mut self.require_calls);
135 Self::release_vec(&mut self.package_path_references);
136 Self::release_vec(&mut self.whole_object_uses);
137 Self::release_vec(&mut self.unused_import_bindings);
138 Self::release_vec(&mut self.type_referenced_import_bindings);
139 Self::release_vec(&mut self.value_referenced_import_bindings);
140 Self::release_vec(&mut self.namespace_object_aliases);
141 Self::release_vec(&mut self.auto_import_candidates);
142 }
143
144 fn release_vec<T>(values: &mut Vec<T>) {
145 *values = Vec::new();
146 }
147}
148
149#[derive(
151 Debug,
152 Clone,
153 Copy,
154 PartialEq,
155 Eq,
156 PartialOrd,
157 Ord,
158 serde::Serialize,
159 serde::Deserialize,
160 bitcode::Encode,
161 bitcode::Decode,
162)]
163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
164#[serde(rename_all = "kebab-case")]
165pub enum SecurityControlKind {
166 Sanitization,
168 Validation,
170 Authentication,
172 Authorization,
174}
175
176#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
178pub struct SecurityControlSite {
179 pub kind: SecurityControlKind,
181 pub callee_path: String,
184 pub span_start: u32,
186 pub span_end: u32,
188}
189
190#[derive(
193 Debug,
194 Clone,
195 Copy,
196 PartialEq,
197 Eq,
198 PartialOrd,
199 Ord,
200 serde::Serialize,
201 serde::Deserialize,
202 bitcode::Encode,
203 bitcode::Decode,
204)]
205pub enum SanitizerScope {
206 Html,
208 Url,
210 Path,
212 SqlIdentifier,
214}
215
216#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
218pub struct SanitizedSinkArg {
219 pub span_start: u32,
221 pub arg_index: u32,
223 pub scope: SanitizerScope,
225}
226
227#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
236pub struct TaintedBinding {
237 pub local: String,
239 pub source_path: String,
241 pub source_span_start: u32,
249}
250
251#[derive(
254 Debug,
255 Clone,
256 Copy,
257 PartialEq,
258 Eq,
259 PartialOrd,
260 Ord,
261 serde::Serialize,
262 serde::Deserialize,
263 bitcode::Encode,
264 bitcode::Decode,
265)]
266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
267#[serde(rename_all = "kebab-case")]
268pub enum SkippedSecurityCalleeReason {
269 ComputedMember,
271 DynamicDispatch,
273 UnsupportedAssignmentObject,
275}
276
277#[derive(
279 Debug,
280 Clone,
281 Copy,
282 PartialEq,
283 Eq,
284 PartialOrd,
285 Ord,
286 serde::Serialize,
287 serde::Deserialize,
288 bitcode::Encode,
289 bitcode::Decode,
290)]
291#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
292#[serde(rename_all = "kebab-case")]
293pub enum SkippedSecurityCalleeExpressionKind {
294 StaticMemberExpression,
296 ComputedMemberExpression,
298 Identifier,
300 Other,
302}
303
304#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
306pub struct SkippedSecurityCalleeSite {
307 pub reason: SkippedSecurityCalleeReason,
309 pub expression_kind: SkippedSecurityCalleeExpressionKind,
311 pub span_start: u32,
313 pub span_end: u32,
315}
316
317#[derive(
322 Debug,
323 Clone,
324 Copy,
325 PartialEq,
326 Eq,
327 serde::Serialize,
328 serde::Deserialize,
329 bitcode::Encode,
330 bitcode::Decode,
331)]
332pub enum SinkShape {
333 Call,
335 MemberCall,
337 MemberAssign,
339 TaggedTemplate,
341 JsxAttr,
343 NewExpression,
345 SecretLiteral,
348}
349
350#[derive(
357 Debug,
358 Clone,
359 Copy,
360 PartialEq,
361 Eq,
362 serde::Serialize,
363 serde::Deserialize,
364 bitcode::Encode,
365 bitcode::Decode,
366)]
367pub enum SinkArgKind {
368 TemplateWithSubst,
372 Concat,
374 Object,
376 Call,
378 Literal,
380 NoArg,
382 Other,
384}
385
386#[derive(
388 Debug,
389 Clone,
390 Copy,
391 PartialEq,
392 Eq,
393 serde::Serialize,
394 serde::Deserialize,
395 bitcode::Encode,
396 bitcode::Decode,
397)]
398#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
399#[serde(rename_all = "kebab-case")]
400pub enum SecurityUrlShape {
401 FixedOriginDynamicPath,
404 DynamicOrigin,
406}
407
408#[derive(
410 Debug,
411 Clone,
412 PartialEq,
413 Eq,
414 serde::Serialize,
415 serde::Deserialize,
416 bitcode::Encode,
417 bitcode::Decode,
418)]
419pub enum SinkLiteralValue {
420 String(String),
422 Integer(i64),
424 Boolean(bool),
426 Null,
428}
429
430#[derive(
433 Debug,
434 Clone,
435 PartialEq,
436 Eq,
437 serde::Serialize,
438 serde::Deserialize,
439 bitcode::Encode,
440 bitcode::Decode,
441)]
442pub struct SinkObjectProperty {
443 pub key: String,
445 pub value: SinkLiteralValue,
447}
448
449#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
454pub struct SinkSite {
455 pub sink_shape: SinkShape,
457 pub callee_path: String,
459 pub arg_index: u32,
461 pub arg_is_non_literal: bool,
464 pub arg_kind: SinkArgKind,
469 pub arg_literal: Option<SinkLiteralValue>,
471 pub regex_pattern: Option<String>,
473 pub object_properties: Vec<SinkObjectProperty>,
475 pub object_property_keys: Vec<String>,
478 pub object_property_keys_complete: bool,
482 pub arg_idents: Vec<String>,
489 pub arg_source_paths: Vec<String>,
495 pub span_start: u32,
498 pub span_end: u32,
500 pub url_arg_literal: Option<String>,
507 pub url_shape: Option<SecurityUrlShape>,
511}
512
513impl SinkSite {
514 #[must_use]
516 pub fn span(&self) -> Span {
517 Span::new(self.span_start, self.span_end)
518 }
519}
520
521pub const PUBLIC_ENV_PREFIXES: &[&str] = &[
526 "NEXT_PUBLIC_",
527 "VITE_",
528 "NUXT_PUBLIC_",
529 "REACT_APP_",
530 "PUBLIC_",
531 "GATSBY_",
532 "EXPO_PUBLIC_",
533 "STORYBOOK_",
534];
535
536pub const PUBLIC_ENV_EXACT: &[&str] = &["NODE_ENV"];
538
539pub const PUBLIC_ENV_METADATA_TOKENS: &[&str] =
542 &["BRANCH", "ENVIRONMENT", "MODE", "REF", "SHA", "TAG"];
543
544pub const SECRET_ENV_TOKENS: &[&str] = &[
547 "AUTH",
548 "CREDENTIAL",
549 "CREDENTIALS",
550 "KEY",
551 "PASS",
552 "PASSWORD",
553 "PRIVATE",
554 "SECRET",
555 "TOKEN",
556];
557
558fn env_name_has_token(name: &str, tokens: &[&str]) -> bool {
559 name.split(|ch: char| !ch.is_ascii_alphanumeric())
560 .filter(|part| !part.is_empty())
561 .any(|part| tokens.contains(&part))
562}
563
564#[must_use]
567pub fn is_public_env_var(name: &str) -> bool {
568 if PUBLIC_ENV_EXACT.contains(&name) || PUBLIC_ENV_PREFIXES.iter().any(|p| name.starts_with(p)) {
569 return true;
570 }
571 env_name_has_token(name, PUBLIC_ENV_METADATA_TOKENS)
572 && !env_name_has_token(name, SECRET_ENV_TOKENS)
573}
574
575#[must_use]
579pub fn is_public_env_path(path: &str) -> bool {
580 for object in ["process.env.", "import.meta.env."] {
581 if let Some(var) = path.strip_prefix(object) {
582 return is_public_env_var(var);
583 }
584 }
585 false
586}
587
588#[derive(Debug, Clone)]
590pub struct NamespaceObjectAlias {
591 pub via_export_name: String,
593 pub suffix: String,
595 pub namespace_local: String,
597}
598
599#[must_use]
601#[expect(
602 clippy::cast_possible_truncation,
603 reason = "source files are practically < 4GB"
604)]
605pub fn compute_line_offsets(source: &str) -> Vec<u32> {
606 let mut offsets = vec![0u32];
607 for (i, byte) in source.bytes().enumerate() {
608 if byte == b'\n' {
609 debug_assert!(
610 u32::try_from(i + 1).is_ok(),
611 "source file exceeds u32::MAX bytes โ line offsets would overflow"
612 );
613 offsets.push((i + 1) as u32);
614 }
615 }
616 offsets
617}
618
619#[must_use]
621#[expect(
622 clippy::cast_possible_truncation,
623 reason = "line count is bounded by source size"
624)]
625pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
626 let line_idx = match line_offsets.binary_search(&byte_offset) {
627 Ok(idx) => idx,
628 Err(idx) => idx.saturating_sub(1),
629 };
630 let line = line_idx as u32 + 1;
631 let col = byte_offset - line_offsets[line_idx];
632 (line, col)
633}
634
635#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
637pub struct FunctionComplexity {
638 pub name: String,
640 pub line: u32,
642 pub col: u32,
644 pub cyclomatic: u16,
646 pub cognitive: u16,
648 pub line_count: u32,
650 pub param_count: u8,
652 pub source_hash: Option<String>,
654 pub contributions: Vec<ComplexityContribution>,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
664#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
665#[serde(rename_all = "kebab-case")]
666pub enum ComplexityMetric {
667 Cyclomatic,
669 Cognitive,
671}
672
673#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
679#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
680#[serde(rename_all = "kebab-case")]
681pub enum ComplexityContributionKind {
682 If,
684 Else,
686 ElseIf,
689 Ternary,
691 LogicalAnd,
693 LogicalOr,
695 NullishCoalescing,
697 LogicalAssignment,
699 OptionalChain,
701 For,
703 ForIn,
705 ForOf,
707 While,
709 DoWhile,
711 Switch,
713 Case,
715 Catch,
717 LabeledBreak,
719 LabeledContinue,
721}
722
723#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
731pub struct ComplexityContribution {
732 pub line: u32,
734 pub col: u32,
736 pub metric: ComplexityMetric,
738 pub kind: ComplexityContributionKind,
740 pub weight: u16,
743 pub nesting: u16,
746}
747
748#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
750pub enum FlagUseKind {
751 EnvVar,
753 SdkCall,
755 ConfigObject,
757}
758
759#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
761pub struct FlagUse {
762 pub flag_name: String,
764 pub kind: FlagUseKind,
766 pub line: u32,
768 pub col: u32,
770 pub guard_span_start: Option<u32>,
772 pub guard_span_end: Option<u32>,
774 pub sdk_name: Option<String>,
776}
777
778const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
779
780#[derive(Debug, Clone)]
782pub struct DynamicImportPattern {
783 pub prefix: String,
785 pub suffix: Option<String>,
787 pub span: Span,
789}
790
791#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
793#[serde(rename_all = "lowercase")]
794#[repr(u8)]
795pub enum VisibilityTag {
796 #[default]
798 None = 0,
799 Public = 1,
801 Internal = 2,
803 Beta = 3,
805 Alpha = 4,
807 ExpectedUnused = 5,
809}
810
811impl VisibilityTag {
812 pub const fn suppresses_unused(self) -> bool {
816 matches!(
817 self,
818 Self::Public | Self::Internal | Self::Beta | Self::Alpha
819 )
820 }
821
822 pub fn is_none(&self) -> bool {
824 matches!(self, Self::None)
825 }
826}
827
828#[derive(Debug, Clone, serde::Serialize)]
830pub struct ExportInfo {
831 pub name: ExportName,
833 pub local_name: Option<String>,
835 pub is_type_only: bool,
837 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
839 pub is_side_effect_used: bool,
840 #[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
842 pub visibility: VisibilityTag,
843 #[serde(serialize_with = "serialize_span")]
845 pub span: Span,
846 #[serde(default, skip_serializing_if = "Vec::is_empty")]
848 pub members: Vec<MemberInfo>,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub super_class: Option<String>,
852}
853
854#[derive(
856 Debug,
857 Clone,
858 serde::Serialize,
859 serde::Deserialize,
860 bitcode::Encode,
861 bitcode::Decode,
862 PartialEq,
863 Eq,
864)]
865pub struct ClassHeritageInfo {
866 pub export_name: String,
868 pub super_class: Option<String>,
870 pub implements: Vec<String>,
872 #[serde(default, skip_serializing_if = "Vec::is_empty")]
874 pub instance_bindings: Vec<(String, String)>,
875}
876
877#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
879pub struct LocalTypeDeclaration {
880 pub name: String,
882 #[serde(serialize_with = "serialize_span")]
884 pub span: Span,
885}
886
887#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
889pub struct PublicSignatureTypeReference {
890 pub export_name: String,
892 pub type_name: String,
894 #[serde(serialize_with = "serialize_span")]
896 pub span: Span,
897}
898
899#[derive(Debug, Clone, serde::Serialize)]
901pub struct MemberInfo {
902 pub name: String,
904 pub kind: MemberKind,
906 #[serde(serialize_with = "serialize_span")]
908 pub span: Span,
909 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
914 pub has_decorator: bool,
915 #[serde(default, skip_serializing_if = "Vec::is_empty")]
922 pub decorator_names: Vec<String>,
923 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
930 pub is_instance_returning_static: bool,
931 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
939 pub is_self_returning: bool,
940}
941
942#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
944#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
945#[serde(rename_all = "snake_case")]
946pub enum MemberKind {
947 EnumMember,
949 ClassMethod,
951 ClassProperty,
953 NamespaceMember,
955}
956
957#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
959pub struct MemberAccess {
960 pub object: String,
962 pub member: String,
964}
965
966#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
971pub struct CalleeUse {
972 pub callee_path: String,
974 pub span_start: u32,
976}
977
978#[expect(
979 clippy::trivially_copy_pass_by_ref,
980 reason = "serde serialize_with requires &T"
981)]
982fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
983 use serde::ser::SerializeMap;
984 let mut map = serializer.serialize_map(Some(2))?;
985 map.serialize_entry("start", &span.start)?;
986 map.serialize_entry("end", &span.end)?;
987 map.end()
988}
989
990#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
992pub enum ExportName {
993 Named(String),
995 Default,
997}
998
999impl ExportName {
1000 #[must_use]
1002 pub fn matches_str(&self, s: &str) -> bool {
1003 match self {
1004 Self::Named(n) => n == s,
1005 Self::Default => s == "default",
1006 }
1007 }
1008}
1009
1010impl std::fmt::Display for ExportName {
1011 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1012 match self {
1013 Self::Named(n) => write!(f, "{n}"),
1014 Self::Default => write!(f, "default"),
1015 }
1016 }
1017}
1018
1019#[derive(Debug, Clone)]
1021pub struct ImportInfo {
1022 pub source: String,
1024 pub imported_name: ImportedName,
1026 pub local_name: String,
1028 pub is_type_only: bool,
1030 pub from_style: bool,
1032 pub span: Span,
1034 pub source_span: Span,
1036}
1037
1038#[derive(Debug, Clone, PartialEq, Eq)]
1040pub enum ImportedName {
1041 Named(String),
1043 Default,
1045 Namespace,
1047 SideEffect,
1049}
1050
1051#[cfg(target_pointer_width = "64")]
1052const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
1053#[cfg(target_pointer_width = "64")]
1054const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
1055#[cfg(target_pointer_width = "64")]
1056const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
1057#[cfg(target_pointer_width = "64")]
1058const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
1059#[cfg(target_pointer_width = "64")]
1060const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
1061#[cfg(target_pointer_width = "64")]
1062const _: () = assert!(std::mem::size_of::<SinkSite>() == 216);
1063#[cfg(target_pointer_width = "64")]
1064const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 792);
1065
1066#[derive(Debug, Clone)]
1068pub struct ReExportInfo {
1069 pub source: String,
1071 pub imported_name: String,
1073 pub exported_name: String,
1075 pub is_type_only: bool,
1077 pub span: oxc_span::Span,
1079}
1080
1081#[derive(Debug, Clone)]
1083pub struct DynamicImportInfo {
1084 pub source: String,
1086 pub span: Span,
1088 pub destructured_names: Vec<String>,
1092 pub local_name: Option<String>,
1095 pub is_speculative: bool,
1097}
1098
1099#[derive(Debug, Clone)]
1101pub struct RequireCallInfo {
1102 pub source: String,
1104 pub span: Span,
1106 pub source_span: Span,
1112 pub destructured_names: Vec<String>,
1114 pub local_name: Option<String>,
1116}
1117
1118pub struct ParseResult {
1120 pub modules: Vec<ModuleInfo>,
1122 pub cache_hits: usize,
1124 pub cache_misses: usize,
1126 pub parse_cpu_ms: f64,
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133
1134 fn span() -> Span {
1135 Span::new(0, 1)
1136 }
1137
1138 macro_rules! assert_released {
1139 ($values:expr) => {{
1140 assert!($values.is_empty());
1141 assert_eq!($values.capacity(), 0);
1142 }};
1143 }
1144
1145 #[test]
1146 fn public_env_var_includes_public_ci_metadata() {
1147 for name in ["TAG_REF", "GITHUB_SHA", "CI_COMMIT_BRANCH", "APP_MODE"] {
1148 assert!(is_public_env_var(name), "{name} should be public metadata");
1149 }
1150 }
1151
1152 #[test]
1153 fn public_env_var_keeps_secret_shaped_names_source_backed() {
1154 for name in ["GITHUB_TOKEN", "REFRESH_TOKEN", "API_KEY", "SECRET_SHA"] {
1155 assert!(
1156 !is_public_env_var(name),
1157 "{name} should remain secret-shaped"
1158 );
1159 }
1160 }
1161
1162 #[test]
1163 fn line_offsets_empty_string() {
1164 assert_eq!(compute_line_offsets(""), vec![0]);
1165 }
1166
1167 #[test]
1168 fn release_resolution_payload_drops_copied_vectors_only() {
1169 let mut module = ModuleInfo {
1170 file_id: FileId(7),
1171 exports: vec![ExportInfo {
1172 name: ExportName::Named("kept".to_string()),
1173 local_name: None,
1174 is_type_only: false,
1175 is_side_effect_used: false,
1176 visibility: VisibilityTag::None,
1177 span: span(),
1178 members: Vec::new(),
1179 super_class: None,
1180 }],
1181 imports: vec![ImportInfo {
1182 source: "node:child_process".to_string(),
1183 imported_name: ImportedName::Default,
1184 local_name: "childProcess".to_string(),
1185 is_type_only: false,
1186 from_style: false,
1187 span: span(),
1188 source_span: span(),
1189 }],
1190 re_exports: vec![ReExportInfo {
1191 source: "./kept".to_string(),
1192 imported_name: "kept".to_string(),
1193 exported_name: "kept".to_string(),
1194 is_type_only: false,
1195 span: span(),
1196 }],
1197 dynamic_imports: vec![DynamicImportInfo {
1198 source: "./dynamic".to_string(),
1199 span: span(),
1200 destructured_names: vec!["value".to_string()],
1201 local_name: None,
1202 is_speculative: false,
1203 }],
1204 dynamic_import_patterns: vec![DynamicImportPattern {
1205 prefix: "./pages/".to_string(),
1206 suffix: Some(".tsx".to_string()),
1207 span: span(),
1208 }],
1209 require_calls: vec![RequireCallInfo {
1210 source: "./required".to_string(),
1211 span: span(),
1212 source_span: span(),
1213 destructured_names: Vec::new(),
1214 local_name: Some("required".to_string()),
1215 }],
1216 package_path_references: vec!["react".to_string()],
1217 member_accesses: vec![MemberAccess {
1218 object: "Status".to_string(),
1219 member: "Active".to_string(),
1220 }],
1221 whole_object_uses: vec!["Status".to_string()],
1222 has_cjs_exports: true,
1223 has_angular_component_template_url: true,
1224 content_hash: 42,
1225 suppressions: Vec::new(),
1226 unknown_suppression_kinds: Vec::new(),
1227 unused_import_bindings: vec!["unused".to_string()],
1228 type_referenced_import_bindings: vec!["TypeOnly".to_string()],
1229 value_referenced_import_bindings: vec!["Value".to_string()],
1230 line_offsets: vec![0, 8],
1231 complexity: vec![FunctionComplexity {
1232 name: "work".to_string(),
1233 line: 1,
1234 col: 0,
1235 cyclomatic: 2,
1236 cognitive: 3,
1237 line_count: 4,
1238 param_count: 1,
1239 source_hash: Some("hash".to_string()),
1240 contributions: Vec::new(),
1241 }],
1242 flag_uses: vec![FlagUse {
1243 flag_name: "FEATURE_X".to_string(),
1244 kind: FlagUseKind::EnvVar,
1245 line: 1,
1246 col: 0,
1247 guard_span_start: None,
1248 guard_span_end: None,
1249 sdk_name: None,
1250 }],
1251 class_heritage: vec![ClassHeritageInfo {
1252 export_name: "Child".to_string(),
1253 super_class: Some("Parent".to_string()),
1254 implements: vec!["Contract".to_string()],
1255 instance_bindings: Vec::new(),
1256 }],
1257 injection_tokens: vec![("TOKEN".to_string(), "Contract".to_string())],
1258 local_type_declarations: vec![LocalTypeDeclaration {
1259 name: "Contract".to_string(),
1260 span: span(),
1261 }],
1262 public_signature_type_references: vec![PublicSignatureTypeReference {
1263 export_name: "kept".to_string(),
1264 type_name: "Contract".to_string(),
1265 span: span(),
1266 }],
1267 namespace_object_aliases: vec![NamespaceObjectAlias {
1268 via_export_name: "api".to_string(),
1269 suffix: "read".to_string(),
1270 namespace_local: "ns".to_string(),
1271 }],
1272 iconify_prefixes: vec!["hero".to_string()],
1273 iconify_icon_names: vec!["hero-home".to_string()],
1274 auto_import_candidates: vec!["useState".to_string()],
1275 directives: vec!["use client".to_string()],
1276 security_sinks: Vec::new(),
1277 security_sinks_skipped: 1,
1278 security_unresolved_callee_sites: Vec::new(),
1279 tainted_bindings: Vec::new(),
1280 sanitized_sink_args: Vec::new(),
1281 security_control_sites: Vec::new(),
1282 callee_uses: Vec::new(),
1283 };
1284
1285 module.release_resolution_payload();
1286
1287 assert_eq!(module.file_id, FileId(7));
1288 assert_eq!(module.content_hash, 42);
1289 assert_eq!(module.line_offsets, vec![0, 8]);
1290 assert_eq!(module.imports.len(), 1);
1291 assert_eq!(module.exports.len(), 1);
1292 assert_eq!(module.re_exports.len(), 1);
1293 assert_eq!(module.dynamic_import_patterns.len(), 1);
1294 assert_eq!(module.member_accesses.len(), 1);
1295 assert_eq!(module.complexity.len(), 1);
1296 assert_eq!(module.flag_uses.len(), 1);
1297 assert_eq!(module.class_heritage.len(), 1);
1298 assert_eq!(module.injection_tokens.len(), 1);
1299 assert_eq!(module.local_type_declarations.len(), 1);
1300 assert_eq!(module.public_signature_type_references.len(), 1);
1301 assert_eq!(module.iconify_prefixes.len(), 1);
1302 assert_eq!(module.iconify_icon_names.len(), 1);
1303 assert_eq!(module.directives.len(), 1);
1304 assert_eq!(module.security_sinks_skipped, 1);
1305 assert_released!(module.dynamic_imports);
1306 assert_released!(module.require_calls);
1307 assert_released!(module.package_path_references);
1308 assert_released!(module.whole_object_uses);
1309 assert_released!(module.unused_import_bindings);
1310 assert_released!(module.type_referenced_import_bindings);
1311 assert_released!(module.value_referenced_import_bindings);
1312 assert_released!(module.namespace_object_aliases);
1313 assert_released!(module.auto_import_candidates);
1314 }
1315
1316 #[test]
1317 fn sink_shape_bitcode_roundtrip() {
1318 for shape in [
1319 SinkShape::Call,
1320 SinkShape::MemberCall,
1321 SinkShape::MemberAssign,
1322 SinkShape::TaggedTemplate,
1323 SinkShape::JsxAttr,
1324 SinkShape::NewExpression,
1325 SinkShape::SecretLiteral,
1326 ] {
1327 let encoded = bitcode::encode(&shape);
1328 let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
1329 assert_eq!(shape, decoded);
1330 }
1331 }
1332
1333 #[test]
1334 fn sink_arg_kind_bitcode_roundtrip() {
1335 for kind in [
1336 SinkArgKind::TemplateWithSubst,
1337 SinkArgKind::Concat,
1338 SinkArgKind::Object,
1339 SinkArgKind::Call,
1340 SinkArgKind::Literal,
1341 SinkArgKind::NoArg,
1342 SinkArgKind::Other,
1343 ] {
1344 let encoded = bitcode::encode(&kind);
1345 let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
1346 assert_eq!(kind, decoded);
1347 }
1348 }
1349
1350 #[test]
1351 fn security_url_shape_bitcode_roundtrip() {
1352 for shape in [
1353 SecurityUrlShape::FixedOriginDynamicPath,
1354 SecurityUrlShape::DynamicOrigin,
1355 ] {
1356 let encoded = bitcode::encode(&shape);
1357 let decoded: SecurityUrlShape =
1358 bitcode::decode(&encoded).expect("decode security url shape");
1359 assert_eq!(shape, decoded);
1360 }
1361 }
1362
1363 #[test]
1364 fn sink_site_bitcode_roundtrip() {
1365 let site = SinkSite {
1366 sink_shape: SinkShape::MemberAssign,
1367 callee_path: "el.innerHTML".to_string(),
1368 arg_index: 0,
1369 arg_is_non_literal: true,
1370 arg_kind: SinkArgKind::Other,
1371 arg_literal: Some(SinkLiteralValue::Integer(511)),
1372 regex_pattern: None,
1373 object_properties: vec![SinkObjectProperty {
1374 key: "origin".to_string(),
1375 value: SinkLiteralValue::String("*".to_string()),
1376 }],
1377 object_property_keys: vec!["origin".to_string()],
1378 object_property_keys_complete: true,
1379 arg_idents: vec!["userInput".to_string()],
1380 arg_source_paths: vec!["req.body.email".to_string(), "req.body".to_string()],
1381 span_start: 10,
1382 span_end: 20,
1383 url_arg_literal: Some("https://api.example.com".to_string()),
1384 url_shape: Some(SecurityUrlShape::FixedOriginDynamicPath),
1385 };
1386 let encoded = bitcode::encode(&site);
1387 let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
1388 assert_eq!(decoded.sink_shape, site.sink_shape);
1389 assert_eq!(decoded.callee_path, site.callee_path);
1390 assert_eq!(decoded.arg_index, site.arg_index);
1391 assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
1392 assert_eq!(decoded.arg_kind, site.arg_kind);
1393 assert_eq!(decoded.arg_literal, site.arg_literal);
1394 assert_eq!(decoded.object_properties, site.object_properties);
1395 assert_eq!(decoded.object_property_keys, site.object_property_keys);
1396 assert_eq!(
1397 decoded.object_property_keys_complete,
1398 site.object_property_keys_complete
1399 );
1400 assert_eq!(decoded.arg_idents, site.arg_idents);
1401 assert_eq!(decoded.arg_source_paths, site.arg_source_paths);
1402 assert_eq!(decoded.url_shape, site.url_shape);
1403 assert_eq!(decoded.span(), site.span());
1404 }
1405
1406 #[test]
1407 fn line_offsets_single_line_no_newline() {
1408 assert_eq!(compute_line_offsets("hello"), vec![0]);
1409 }
1410
1411 #[test]
1412 fn line_offsets_single_line_with_newline() {
1413 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
1414 }
1415
1416 #[test]
1417 fn line_offsets_multiple_lines() {
1418 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
1419 }
1420
1421 #[test]
1422 fn line_offsets_trailing_newline() {
1423 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
1424 }
1425
1426 #[test]
1427 fn line_offsets_consecutive_newlines() {
1428 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
1429 }
1430
1431 #[test]
1432 fn line_offsets_multibyte_utf8() {
1433 assert_eq!(compute_line_offsets("รก\n"), vec![0, 3]);
1434 }
1435
1436 #[test]
1437 fn line_col_offset_zero() {
1438 let offsets = compute_line_offsets("abc\ndef\nghi");
1439 let (line, col) = byte_offset_to_line_col(&offsets, 0);
1440 assert_eq!((line, col), (1, 0));
1441 }
1442
1443 #[test]
1444 fn line_col_middle_of_first_line() {
1445 let offsets = compute_line_offsets("abc\ndef\nghi");
1446 let (line, col) = byte_offset_to_line_col(&offsets, 2);
1447 assert_eq!((line, col), (1, 2));
1448 }
1449
1450 #[test]
1451 fn line_col_start_of_second_line() {
1452 let offsets = compute_line_offsets("abc\ndef\nghi");
1453 let (line, col) = byte_offset_to_line_col(&offsets, 4);
1454 assert_eq!((line, col), (2, 0));
1455 }
1456
1457 #[test]
1458 fn line_col_middle_of_second_line() {
1459 let offsets = compute_line_offsets("abc\ndef\nghi");
1460 let (line, col) = byte_offset_to_line_col(&offsets, 5);
1461 assert_eq!((line, col), (2, 1));
1462 }
1463
1464 #[test]
1465 fn line_col_start_of_third_line() {
1466 let offsets = compute_line_offsets("abc\ndef\nghi");
1467 let (line, col) = byte_offset_to_line_col(&offsets, 8);
1468 assert_eq!((line, col), (3, 0));
1469 }
1470
1471 #[test]
1472 fn line_col_end_of_file() {
1473 let offsets = compute_line_offsets("abc\ndef\nghi");
1474 let (line, col) = byte_offset_to_line_col(&offsets, 10);
1475 assert_eq!((line, col), (3, 2));
1476 }
1477
1478 #[test]
1479 fn line_col_single_line() {
1480 let offsets = compute_line_offsets("hello");
1481 let (line, col) = byte_offset_to_line_col(&offsets, 3);
1482 assert_eq!((line, col), (1, 3));
1483 }
1484
1485 #[test]
1486 fn line_col_at_newline_byte() {
1487 let offsets = compute_line_offsets("abc\ndef");
1488 let (line, col) = byte_offset_to_line_col(&offsets, 3);
1489 assert_eq!((line, col), (1, 3));
1490 }
1491
1492 #[test]
1493 fn export_name_matches_str_named() {
1494 let name = ExportName::Named("foo".to_string());
1495 assert!(name.matches_str("foo"));
1496 assert!(!name.matches_str("bar"));
1497 assert!(!name.matches_str("default"));
1498 }
1499
1500 #[test]
1501 fn export_name_matches_str_default() {
1502 let name = ExportName::Default;
1503 assert!(name.matches_str("default"));
1504 assert!(!name.matches_str("foo"));
1505 }
1506
1507 #[test]
1508 fn export_name_display_named() {
1509 let name = ExportName::Named("myExport".to_string());
1510 assert_eq!(name.to_string(), "myExport");
1511 }
1512
1513 #[test]
1514 fn export_name_display_default() {
1515 let name = ExportName::Default;
1516 assert_eq!(name.to_string(), "default");
1517 }
1518
1519 #[test]
1520 fn export_name_equality_named() {
1521 let a = ExportName::Named("foo".to_string());
1522 let b = ExportName::Named("foo".to_string());
1523 let c = ExportName::Named("bar".to_string());
1524 assert_eq!(a, b);
1525 assert_ne!(a, c);
1526 }
1527
1528 #[test]
1529 fn export_name_equality_default() {
1530 let a = ExportName::Default;
1531 let b = ExportName::Default;
1532 assert_eq!(a, b);
1533 }
1534
1535 #[test]
1536 fn export_name_named_not_equal_to_default() {
1537 let named = ExportName::Named("default".to_string());
1538 let default = ExportName::Default;
1539 assert_ne!(named, default);
1540 }
1541
1542 #[test]
1543 fn export_name_hash_consistency() {
1544 use std::collections::hash_map::DefaultHasher;
1545 use std::hash::{Hash, Hasher};
1546
1547 let mut h1 = DefaultHasher::new();
1548 let mut h2 = DefaultHasher::new();
1549 ExportName::Named("foo".to_string()).hash(&mut h1);
1550 ExportName::Named("foo".to_string()).hash(&mut h2);
1551 assert_eq!(h1.finish(), h2.finish());
1552 }
1553
1554 #[test]
1555 fn export_name_matches_str_empty_string() {
1556 let name = ExportName::Named(String::new());
1557 assert!(name.matches_str(""));
1558 assert!(!name.matches_str("foo"));
1559 }
1560
1561 #[test]
1562 fn export_name_default_does_not_match_empty() {
1563 let name = ExportName::Default;
1564 assert!(!name.matches_str(""));
1565 }
1566
1567 #[test]
1568 fn imported_name_equality() {
1569 assert_eq!(
1570 ImportedName::Named("foo".to_string()),
1571 ImportedName::Named("foo".to_string())
1572 );
1573 assert_ne!(
1574 ImportedName::Named("foo".to_string()),
1575 ImportedName::Named("bar".to_string())
1576 );
1577 assert_eq!(ImportedName::Default, ImportedName::Default);
1578 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
1579 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
1580 assert_ne!(ImportedName::Default, ImportedName::Namespace);
1581 assert_ne!(
1582 ImportedName::Named("default".to_string()),
1583 ImportedName::Default
1584 );
1585 }
1586
1587 #[test]
1588 fn member_kind_equality() {
1589 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
1590 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
1591 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
1592 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
1593 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
1594 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
1595 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
1596 }
1597
1598 #[test]
1599 fn member_kind_bitcode_roundtrip() {
1600 let kinds = [
1601 MemberKind::EnumMember,
1602 MemberKind::ClassMethod,
1603 MemberKind::ClassProperty,
1604 MemberKind::NamespaceMember,
1605 ];
1606 for kind in &kinds {
1607 let bytes = bitcode::encode(kind);
1608 let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
1609 assert_eq!(&decoded, kind);
1610 }
1611 }
1612
1613 #[test]
1614 fn member_access_bitcode_roundtrip() {
1615 let access = MemberAccess {
1616 object: "Status".to_string(),
1617 member: "Active".to_string(),
1618 };
1619 let bytes = bitcode::encode(&access);
1620 let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
1621 assert_eq!(decoded.object, "Status");
1622 assert_eq!(decoded.member, "Active");
1623 }
1624
1625 #[test]
1626 fn line_offsets_crlf_only_counts_lf() {
1627 let offsets = compute_line_offsets("ab\r\ncd");
1628 assert_eq!(offsets, vec![0, 4]);
1629 }
1630
1631 #[test]
1632 fn line_col_empty_file_offset_zero() {
1633 let offsets = compute_line_offsets("");
1634 let (line, col) = byte_offset_to_line_col(&offsets, 0);
1635 assert_eq!((line, col), (1, 0));
1636 }
1637
1638 #[test]
1639 fn function_complexity_bitcode_roundtrip() {
1640 let fc = FunctionComplexity {
1641 name: "processData".to_string(),
1642 line: 42,
1643 col: 4,
1644 cyclomatic: 15,
1645 cognitive: 25,
1646 line_count: 80,
1647 param_count: 3,
1648 source_hash: Some("0123456789abcdef".to_string()),
1649 contributions: vec![
1650 ComplexityContribution {
1651 line: 43,
1652 col: 8,
1653 metric: ComplexityMetric::Cyclomatic,
1654 kind: ComplexityContributionKind::If,
1655 weight: 1,
1656 nesting: 0,
1657 },
1658 ComplexityContribution {
1659 line: 45,
1660 col: 12,
1661 metric: ComplexityMetric::Cognitive,
1662 kind: ComplexityContributionKind::ElseIf,
1663 weight: 3,
1664 nesting: 2,
1665 },
1666 ],
1667 };
1668 let bytes = bitcode::encode(&fc);
1669 let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
1670 assert_eq!(decoded.name, "processData");
1671 assert_eq!(decoded.line, 42);
1672 assert_eq!(decoded.col, 4);
1673 assert_eq!(decoded.cyclomatic, 15);
1674 assert_eq!(decoded.cognitive, 25);
1675 assert_eq!(decoded.line_count, 80);
1676 assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
1677 assert_eq!(decoded.contributions.len(), 2);
1678 assert_eq!(
1679 decoded.contributions[1].kind,
1680 ComplexityContributionKind::ElseIf
1681 );
1682 assert_eq!(decoded.contributions[1].weight, 3);
1683 assert_eq!(decoded.contributions[1].nesting, 2);
1684 assert_eq!(decoded.contributions[1].metric, ComplexityMetric::Cognitive);
1685 }
1686}