use oxc_span::Span;
use crate::discover::FileId;
use crate::suppress::{Suppression, UnknownSuppressionKind};
#[derive(Debug, Clone)]
pub struct ModuleInfo {
pub file_id: FileId,
pub exports: Vec<ExportInfo>,
pub imports: Vec<ImportInfo>,
pub re_exports: Vec<ReExportInfo>,
pub dynamic_imports: Vec<DynamicImportInfo>,
pub dynamic_import_patterns: Vec<DynamicImportPattern>,
pub require_calls: Vec<RequireCallInfo>,
pub package_path_references: Vec<String>,
pub member_accesses: Vec<MemberAccess>,
pub whole_object_uses: Vec<String>,
pub has_cjs_exports: bool,
pub has_angular_component_template_url: bool,
pub content_hash: u64,
pub suppressions: Vec<Suppression>,
pub unknown_suppression_kinds: Vec<UnknownSuppressionKind>,
pub unused_import_bindings: Vec<String>,
pub type_referenced_import_bindings: Vec<String>,
pub value_referenced_import_bindings: Vec<String>,
pub line_offsets: Vec<u32>,
pub complexity: Vec<FunctionComplexity>,
pub flag_uses: Vec<FlagUse>,
pub class_heritage: Vec<ClassHeritageInfo>,
pub injection_tokens: Vec<(String, String)>,
pub local_type_declarations: Vec<LocalTypeDeclaration>,
pub public_signature_type_references: Vec<PublicSignatureTypeReference>,
pub namespace_object_aliases: Vec<NamespaceObjectAlias>,
pub iconify_prefixes: Vec<String>,
pub iconify_icon_names: Vec<String>,
pub auto_import_candidates: Vec<String>,
pub directives: Vec<String>,
pub security_sinks: Vec<SinkSite>,
pub security_sinks_skipped: u32,
pub tainted_bindings: Vec<TaintedBinding>,
pub sanitized_sink_args: Vec<SanitizedSinkArg>,
pub security_control_sites: Vec<SecurityControlSite>,
}
impl ModuleInfo {
pub fn release_resolution_payload(&mut self) {
Self::release_vec(&mut self.dynamic_imports);
Self::release_vec(&mut self.require_calls);
Self::release_vec(&mut self.package_path_references);
Self::release_vec(&mut self.whole_object_uses);
Self::release_vec(&mut self.unused_import_bindings);
Self::release_vec(&mut self.type_referenced_import_bindings);
Self::release_vec(&mut self.value_referenced_import_bindings);
Self::release_vec(&mut self.namespace_object_aliases);
Self::release_vec(&mut self.auto_import_candidates);
}
fn release_vec<T>(values: &mut Vec<T>) {
*values = Vec::new();
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum SecurityControlKind {
Sanitization,
Validation,
Authentication,
Authorization,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
pub struct SecurityControlSite {
pub kind: SecurityControlKind,
pub callee_path: String,
pub span_start: u32,
pub span_end: u32,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
)]
pub enum SanitizerScope {
Html,
Url,
Path,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
pub struct SanitizedSinkArg {
pub span_start: u32,
pub arg_index: u32,
pub scope: SanitizerScope,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
pub struct TaintedBinding {
pub local: String,
pub source_path: String,
pub source_span_start: u32,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
)]
pub enum SinkShape {
Call,
MemberCall,
MemberAssign,
TaggedTemplate,
JsxAttr,
NewExpression,
SecretLiteral,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
)]
pub enum SinkArgKind {
TemplateWithSubst,
Concat,
Object,
Call,
Literal,
NoArg,
Other,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
)]
pub enum SinkLiteralValue {
String(String),
Integer(i64),
Boolean(bool),
Null,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
)]
pub struct SinkObjectProperty {
pub key: String,
pub value: SinkLiteralValue,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
pub struct SinkSite {
pub sink_shape: SinkShape,
pub callee_path: String,
pub arg_index: u32,
pub arg_is_non_literal: bool,
pub arg_kind: SinkArgKind,
pub arg_literal: Option<SinkLiteralValue>,
pub regex_pattern: Option<String>,
pub object_properties: Vec<SinkObjectProperty>,
pub object_property_keys: Vec<String>,
pub object_property_keys_complete: bool,
pub arg_idents: Vec<String>,
pub arg_source_paths: Vec<String>,
pub span_start: u32,
pub span_end: u32,
pub url_arg_literal: Option<String>,
}
impl SinkSite {
#[must_use]
pub fn span(&self) -> Span {
Span::new(self.span_start, self.span_end)
}
}
pub const PUBLIC_ENV_PREFIXES: &[&str] = &[
"NEXT_PUBLIC_",
"VITE_",
"NUXT_PUBLIC_",
"REACT_APP_",
"PUBLIC_",
"GATSBY_",
"EXPO_PUBLIC_",
"STORYBOOK_",
];
pub const PUBLIC_ENV_EXACT: &[&str] = &["NODE_ENV"];
#[must_use]
pub fn is_public_env_var(name: &str) -> bool {
PUBLIC_ENV_EXACT.contains(&name) || PUBLIC_ENV_PREFIXES.iter().any(|p| name.starts_with(p))
}
#[must_use]
pub fn is_public_env_path(path: &str) -> bool {
for object in ["process.env.", "import.meta.env."] {
if let Some(var) = path.strip_prefix(object) {
return is_public_env_var(var);
}
}
false
}
#[derive(Debug, Clone)]
pub struct NamespaceObjectAlias {
pub via_export_name: String,
pub suffix: String,
pub namespace_local: String,
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "source files are practically < 4GB"
)]
pub fn compute_line_offsets(source: &str) -> Vec<u32> {
let mut offsets = vec![0u32];
for (i, byte) in source.bytes().enumerate() {
if byte == b'\n' {
debug_assert!(
u32::try_from(i + 1).is_ok(),
"source file exceeds u32::MAX bytes — line offsets would overflow"
);
offsets.push((i + 1) as u32);
}
}
offsets
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "line count is bounded by source size"
)]
pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
let line_idx = match line_offsets.binary_search(&byte_offset) {
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1),
};
let line = line_idx as u32 + 1;
let col = byte_offset - line_offsets[line_idx];
(line, col)
}
#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
pub struct FunctionComplexity {
pub name: String,
pub line: u32,
pub col: u32,
pub cyclomatic: u16,
pub cognitive: u16,
pub line_count: u32,
pub param_count: u8,
pub source_hash: Option<String>,
pub contributions: Vec<ComplexityContribution>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum ComplexityMetric {
Cyclomatic,
Cognitive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum ComplexityContributionKind {
If,
Else,
ElseIf,
Ternary,
LogicalAnd,
LogicalOr,
NullishCoalescing,
LogicalAssignment,
OptionalChain,
For,
ForIn,
ForOf,
While,
DoWhile,
Switch,
Case,
Catch,
LabeledBreak,
LabeledContinue,
}
#[derive(Debug, Clone, serde::Serialize, bitcode::Encode, bitcode::Decode)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ComplexityContribution {
pub line: u32,
pub col: u32,
pub metric: ComplexityMetric,
pub kind: ComplexityContributionKind,
pub weight: u16,
pub nesting: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
pub enum FlagUseKind {
EnvVar,
SdkCall,
ConfigObject,
}
#[derive(Debug, Clone, bitcode::Encode, bitcode::Decode)]
pub struct FlagUse {
pub flag_name: String,
pub kind: FlagUseKind,
pub line: u32,
pub col: u32,
pub guard_span_start: Option<u32>,
pub guard_span_end: Option<u32>,
pub sdk_name: Option<String>,
}
const _: () = assert!(std::mem::size_of::<FlagUse>() <= 96);
#[derive(Debug, Clone)]
pub struct DynamicImportPattern {
pub prefix: String,
pub suffix: Option<String>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
#[repr(u8)]
pub enum VisibilityTag {
#[default]
None = 0,
Public = 1,
Internal = 2,
Beta = 3,
Alpha = 4,
ExpectedUnused = 5,
}
impl VisibilityTag {
pub const fn suppresses_unused(self) -> bool {
matches!(
self,
Self::Public | Self::Internal | Self::Beta | Self::Alpha
)
}
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ExportInfo {
pub name: ExportName,
pub local_name: Option<String>,
pub is_type_only: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_side_effect_used: bool,
#[serde(default, skip_serializing_if = "VisibilityTag::is_none")]
pub visibility: VisibilityTag,
#[serde(serialize_with = "serialize_span")]
pub span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub members: Vec<MemberInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub super_class: Option<String>,
}
#[derive(
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
bitcode::Encode,
bitcode::Decode,
PartialEq,
Eq,
)]
pub struct ClassHeritageInfo {
pub export_name: String,
pub super_class: Option<String>,
pub implements: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub instance_bindings: Vec<(String, String)>,
}
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
pub struct LocalTypeDeclaration {
pub name: String,
#[serde(serialize_with = "serialize_span")]
pub span: Span,
}
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
pub struct PublicSignatureTypeReference {
pub export_name: String,
pub type_name: String,
#[serde(serialize_with = "serialize_span")]
pub span: Span,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct MemberInfo {
pub name: String,
pub kind: MemberKind,
#[serde(serialize_with = "serialize_span")]
pub span: Span,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub has_decorator: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub decorator_names: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_instance_returning_static: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_self_returning: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum MemberKind {
EnumMember,
ClassMethod,
ClassProperty,
NamespaceMember,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
pub struct MemberAccess {
pub object: String,
pub member: String,
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde serialize_with requires &T"
)]
fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("start", &span.start)?;
map.serialize_entry("end", &span.end)?;
map.end()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
pub enum ExportName {
Named(String),
Default,
}
impl ExportName {
#[must_use]
pub fn matches_str(&self, s: &str) -> bool {
match self {
Self::Named(n) => n == s,
Self::Default => s == "default",
}
}
}
impl std::fmt::Display for ExportName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Named(n) => write!(f, "{n}"),
Self::Default => write!(f, "default"),
}
}
}
#[derive(Debug, Clone)]
pub struct ImportInfo {
pub source: String,
pub imported_name: ImportedName,
pub local_name: String,
pub is_type_only: bool,
pub from_style: bool,
pub span: Span,
pub source_span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportedName {
Named(String),
Default,
Namespace,
SideEffect,
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<ExportInfo>() == 112);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<SinkSite>() == 208);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 744);
#[derive(Debug, Clone)]
pub struct ReExportInfo {
pub source: String,
pub imported_name: String,
pub exported_name: String,
pub is_type_only: bool,
pub span: oxc_span::Span,
}
#[derive(Debug, Clone)]
pub struct DynamicImportInfo {
pub source: String,
pub span: Span,
pub destructured_names: Vec<String>,
pub local_name: Option<String>,
pub is_speculative: bool,
}
#[derive(Debug, Clone)]
pub struct RequireCallInfo {
pub source: String,
pub span: Span,
pub source_span: Span,
pub destructured_names: Vec<String>,
pub local_name: Option<String>,
}
pub struct ParseResult {
pub modules: Vec<ModuleInfo>,
pub cache_hits: usize,
pub cache_misses: usize,
pub parse_cpu_ms: f64,
}
#[cfg(test)]
mod tests {
use super::*;
fn span() -> Span {
Span::new(0, 1)
}
macro_rules! assert_released {
($values:expr) => {{
assert!($values.is_empty());
assert_eq!($values.capacity(), 0);
}};
}
#[test]
fn line_offsets_empty_string() {
assert_eq!(compute_line_offsets(""), vec![0]);
}
#[test]
fn release_resolution_payload_drops_copied_vectors_only() {
let mut module = ModuleInfo {
file_id: FileId(7),
exports: vec![ExportInfo {
name: ExportName::Named("kept".to_string()),
local_name: None,
is_type_only: false,
is_side_effect_used: false,
visibility: VisibilityTag::None,
span: span(),
members: Vec::new(),
super_class: None,
}],
imports: vec![ImportInfo {
source: "node:child_process".to_string(),
imported_name: ImportedName::Default,
local_name: "childProcess".to_string(),
is_type_only: false,
from_style: false,
span: span(),
source_span: span(),
}],
re_exports: vec![ReExportInfo {
source: "./kept".to_string(),
imported_name: "kept".to_string(),
exported_name: "kept".to_string(),
is_type_only: false,
span: span(),
}],
dynamic_imports: vec![DynamicImportInfo {
source: "./dynamic".to_string(),
span: span(),
destructured_names: vec!["value".to_string()],
local_name: None,
is_speculative: false,
}],
dynamic_import_patterns: vec![DynamicImportPattern {
prefix: "./pages/".to_string(),
suffix: Some(".tsx".to_string()),
span: span(),
}],
require_calls: vec![RequireCallInfo {
source: "./required".to_string(),
span: span(),
source_span: span(),
destructured_names: Vec::new(),
local_name: Some("required".to_string()),
}],
package_path_references: vec!["react".to_string()],
member_accesses: vec![MemberAccess {
object: "Status".to_string(),
member: "Active".to_string(),
}],
whole_object_uses: vec!["Status".to_string()],
has_cjs_exports: true,
has_angular_component_template_url: true,
content_hash: 42,
suppressions: Vec::new(),
unknown_suppression_kinds: Vec::new(),
unused_import_bindings: vec!["unused".to_string()],
type_referenced_import_bindings: vec!["TypeOnly".to_string()],
value_referenced_import_bindings: vec!["Value".to_string()],
line_offsets: vec![0, 8],
complexity: vec![FunctionComplexity {
name: "work".to_string(),
line: 1,
col: 0,
cyclomatic: 2,
cognitive: 3,
line_count: 4,
param_count: 1,
source_hash: Some("hash".to_string()),
contributions: Vec::new(),
}],
flag_uses: vec![FlagUse {
flag_name: "FEATURE_X".to_string(),
kind: FlagUseKind::EnvVar,
line: 1,
col: 0,
guard_span_start: None,
guard_span_end: None,
sdk_name: None,
}],
class_heritage: vec![ClassHeritageInfo {
export_name: "Child".to_string(),
super_class: Some("Parent".to_string()),
implements: vec!["Contract".to_string()],
instance_bindings: Vec::new(),
}],
injection_tokens: vec![("TOKEN".to_string(), "Contract".to_string())],
local_type_declarations: vec![LocalTypeDeclaration {
name: "Contract".to_string(),
span: span(),
}],
public_signature_type_references: vec![PublicSignatureTypeReference {
export_name: "kept".to_string(),
type_name: "Contract".to_string(),
span: span(),
}],
namespace_object_aliases: vec![NamespaceObjectAlias {
via_export_name: "api".to_string(),
suffix: "read".to_string(),
namespace_local: "ns".to_string(),
}],
iconify_prefixes: vec!["hero".to_string()],
iconify_icon_names: vec!["hero-home".to_string()],
auto_import_candidates: vec!["useState".to_string()],
directives: vec!["use client".to_string()],
security_sinks: Vec::new(),
security_sinks_skipped: 1,
tainted_bindings: Vec::new(),
sanitized_sink_args: Vec::new(),
security_control_sites: Vec::new(),
};
module.release_resolution_payload();
assert_eq!(module.file_id, FileId(7));
assert_eq!(module.content_hash, 42);
assert_eq!(module.line_offsets, vec![0, 8]);
assert_eq!(module.imports.len(), 1);
assert_eq!(module.exports.len(), 1);
assert_eq!(module.re_exports.len(), 1);
assert_eq!(module.dynamic_import_patterns.len(), 1);
assert_eq!(module.member_accesses.len(), 1);
assert_eq!(module.complexity.len(), 1);
assert_eq!(module.flag_uses.len(), 1);
assert_eq!(module.class_heritage.len(), 1);
assert_eq!(module.injection_tokens.len(), 1);
assert_eq!(module.local_type_declarations.len(), 1);
assert_eq!(module.public_signature_type_references.len(), 1);
assert_eq!(module.iconify_prefixes.len(), 1);
assert_eq!(module.iconify_icon_names.len(), 1);
assert_eq!(module.directives.len(), 1);
assert_eq!(module.security_sinks_skipped, 1);
assert_released!(module.dynamic_imports);
assert_released!(module.require_calls);
assert_released!(module.package_path_references);
assert_released!(module.whole_object_uses);
assert_released!(module.unused_import_bindings);
assert_released!(module.type_referenced_import_bindings);
assert_released!(module.value_referenced_import_bindings);
assert_released!(module.namespace_object_aliases);
assert_released!(module.auto_import_candidates);
}
#[test]
fn sink_shape_bitcode_roundtrip() {
for shape in [
SinkShape::Call,
SinkShape::MemberCall,
SinkShape::MemberAssign,
SinkShape::TaggedTemplate,
SinkShape::JsxAttr,
SinkShape::NewExpression,
SinkShape::SecretLiteral,
] {
let encoded = bitcode::encode(&shape);
let decoded: SinkShape = bitcode::decode(&encoded).expect("decode sink shape");
assert_eq!(shape, decoded);
}
}
#[test]
fn sink_arg_kind_bitcode_roundtrip() {
for kind in [
SinkArgKind::TemplateWithSubst,
SinkArgKind::Concat,
SinkArgKind::Object,
SinkArgKind::Call,
SinkArgKind::Literal,
SinkArgKind::NoArg,
SinkArgKind::Other,
] {
let encoded = bitcode::encode(&kind);
let decoded: SinkArgKind = bitcode::decode(&encoded).expect("decode sink arg kind");
assert_eq!(kind, decoded);
}
}
#[test]
fn sink_site_bitcode_roundtrip() {
let site = SinkSite {
sink_shape: SinkShape::MemberAssign,
callee_path: "el.innerHTML".to_string(),
arg_index: 0,
arg_is_non_literal: true,
arg_kind: SinkArgKind::Other,
arg_literal: Some(SinkLiteralValue::Integer(511)),
regex_pattern: None,
object_properties: vec![SinkObjectProperty {
key: "origin".to_string(),
value: SinkLiteralValue::String("*".to_string()),
}],
object_property_keys: vec!["origin".to_string()],
object_property_keys_complete: true,
arg_idents: vec!["userInput".to_string()],
arg_source_paths: vec!["req.body.email".to_string(), "req.body".to_string()],
span_start: 10,
span_end: 20,
url_arg_literal: Some("https://api.example.com".to_string()),
};
let encoded = bitcode::encode(&site);
let decoded: SinkSite = bitcode::decode(&encoded).expect("decode sink site");
assert_eq!(decoded.sink_shape, site.sink_shape);
assert_eq!(decoded.callee_path, site.callee_path);
assert_eq!(decoded.arg_index, site.arg_index);
assert_eq!(decoded.arg_is_non_literal, site.arg_is_non_literal);
assert_eq!(decoded.arg_kind, site.arg_kind);
assert_eq!(decoded.arg_literal, site.arg_literal);
assert_eq!(decoded.object_properties, site.object_properties);
assert_eq!(decoded.object_property_keys, site.object_property_keys);
assert_eq!(
decoded.object_property_keys_complete,
site.object_property_keys_complete
);
assert_eq!(decoded.arg_idents, site.arg_idents);
assert_eq!(decoded.arg_source_paths, site.arg_source_paths);
assert_eq!(decoded.span(), site.span());
}
#[test]
fn line_offsets_single_line_no_newline() {
assert_eq!(compute_line_offsets("hello"), vec![0]);
}
#[test]
fn line_offsets_single_line_with_newline() {
assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
}
#[test]
fn line_offsets_multiple_lines() {
assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
}
#[test]
fn line_offsets_trailing_newline() {
assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
}
#[test]
fn line_offsets_consecutive_newlines() {
assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
}
#[test]
fn line_offsets_multibyte_utf8() {
assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
}
#[test]
fn line_col_offset_zero() {
let offsets = compute_line_offsets("abc\ndef\nghi");
let (line, col) = byte_offset_to_line_col(&offsets, 0);
assert_eq!((line, col), (1, 0));
}
#[test]
fn line_col_middle_of_first_line() {
let offsets = compute_line_offsets("abc\ndef\nghi");
let (line, col) = byte_offset_to_line_col(&offsets, 2);
assert_eq!((line, col), (1, 2));
}
#[test]
fn line_col_start_of_second_line() {
let offsets = compute_line_offsets("abc\ndef\nghi");
let (line, col) = byte_offset_to_line_col(&offsets, 4);
assert_eq!((line, col), (2, 0));
}
#[test]
fn line_col_middle_of_second_line() {
let offsets = compute_line_offsets("abc\ndef\nghi");
let (line, col) = byte_offset_to_line_col(&offsets, 5);
assert_eq!((line, col), (2, 1));
}
#[test]
fn line_col_start_of_third_line() {
let offsets = compute_line_offsets("abc\ndef\nghi");
let (line, col) = byte_offset_to_line_col(&offsets, 8);
assert_eq!((line, col), (3, 0));
}
#[test]
fn line_col_end_of_file() {
let offsets = compute_line_offsets("abc\ndef\nghi");
let (line, col) = byte_offset_to_line_col(&offsets, 10);
assert_eq!((line, col), (3, 2));
}
#[test]
fn line_col_single_line() {
let offsets = compute_line_offsets("hello");
let (line, col) = byte_offset_to_line_col(&offsets, 3);
assert_eq!((line, col), (1, 3));
}
#[test]
fn line_col_at_newline_byte() {
let offsets = compute_line_offsets("abc\ndef");
let (line, col) = byte_offset_to_line_col(&offsets, 3);
assert_eq!((line, col), (1, 3));
}
#[test]
fn export_name_matches_str_named() {
let name = ExportName::Named("foo".to_string());
assert!(name.matches_str("foo"));
assert!(!name.matches_str("bar"));
assert!(!name.matches_str("default"));
}
#[test]
fn export_name_matches_str_default() {
let name = ExportName::Default;
assert!(name.matches_str("default"));
assert!(!name.matches_str("foo"));
}
#[test]
fn export_name_display_named() {
let name = ExportName::Named("myExport".to_string());
assert_eq!(name.to_string(), "myExport");
}
#[test]
fn export_name_display_default() {
let name = ExportName::Default;
assert_eq!(name.to_string(), "default");
}
#[test]
fn export_name_equality_named() {
let a = ExportName::Named("foo".to_string());
let b = ExportName::Named("foo".to_string());
let c = ExportName::Named("bar".to_string());
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn export_name_equality_default() {
let a = ExportName::Default;
let b = ExportName::Default;
assert_eq!(a, b);
}
#[test]
fn export_name_named_not_equal_to_default() {
let named = ExportName::Named("default".to_string());
let default = ExportName::Default;
assert_ne!(named, default);
}
#[test]
fn export_name_hash_consistency() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h1 = DefaultHasher::new();
let mut h2 = DefaultHasher::new();
ExportName::Named("foo".to_string()).hash(&mut h1);
ExportName::Named("foo".to_string()).hash(&mut h2);
assert_eq!(h1.finish(), h2.finish());
}
#[test]
fn export_name_matches_str_empty_string() {
let name = ExportName::Named(String::new());
assert!(name.matches_str(""));
assert!(!name.matches_str("foo"));
}
#[test]
fn export_name_default_does_not_match_empty() {
let name = ExportName::Default;
assert!(!name.matches_str(""));
}
#[test]
fn imported_name_equality() {
assert_eq!(
ImportedName::Named("foo".to_string()),
ImportedName::Named("foo".to_string())
);
assert_ne!(
ImportedName::Named("foo".to_string()),
ImportedName::Named("bar".to_string())
);
assert_eq!(ImportedName::Default, ImportedName::Default);
assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
assert_ne!(ImportedName::Default, ImportedName::Namespace);
assert_ne!(
ImportedName::Named("default".to_string()),
ImportedName::Default
);
}
#[test]
fn member_kind_equality() {
assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
}
#[test]
fn member_kind_bitcode_roundtrip() {
let kinds = [
MemberKind::EnumMember,
MemberKind::ClassMethod,
MemberKind::ClassProperty,
MemberKind::NamespaceMember,
];
for kind in &kinds {
let bytes = bitcode::encode(kind);
let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
assert_eq!(&decoded, kind);
}
}
#[test]
fn member_access_bitcode_roundtrip() {
let access = MemberAccess {
object: "Status".to_string(),
member: "Active".to_string(),
};
let bytes = bitcode::encode(&access);
let decoded: MemberAccess = bitcode::decode(&bytes).unwrap();
assert_eq!(decoded.object, "Status");
assert_eq!(decoded.member, "Active");
}
#[test]
fn line_offsets_crlf_only_counts_lf() {
let offsets = compute_line_offsets("ab\r\ncd");
assert_eq!(offsets, vec![0, 4]);
}
#[test]
fn line_col_empty_file_offset_zero() {
let offsets = compute_line_offsets("");
let (line, col) = byte_offset_to_line_col(&offsets, 0);
assert_eq!((line, col), (1, 0));
}
#[test]
fn function_complexity_bitcode_roundtrip() {
let fc = FunctionComplexity {
name: "processData".to_string(),
line: 42,
col: 4,
cyclomatic: 15,
cognitive: 25,
line_count: 80,
param_count: 3,
source_hash: Some("0123456789abcdef".to_string()),
contributions: vec![
ComplexityContribution {
line: 43,
col: 8,
metric: ComplexityMetric::Cyclomatic,
kind: ComplexityContributionKind::If,
weight: 1,
nesting: 0,
},
ComplexityContribution {
line: 45,
col: 12,
metric: ComplexityMetric::Cognitive,
kind: ComplexityContributionKind::ElseIf,
weight: 3,
nesting: 2,
},
],
};
let bytes = bitcode::encode(&fc);
let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
assert_eq!(decoded.name, "processData");
assert_eq!(decoded.line, 42);
assert_eq!(decoded.col, 4);
assert_eq!(decoded.cyclomatic, 15);
assert_eq!(decoded.cognitive, 25);
assert_eq!(decoded.line_count, 80);
assert_eq!(decoded.source_hash.as_deref(), Some("0123456789abcdef"));
assert_eq!(decoded.contributions.len(), 2);
assert_eq!(
decoded.contributions[1].kind,
ComplexityContributionKind::ElseIf
);
assert_eq!(decoded.contributions[1].weight, 3);
assert_eq!(decoded.contributions[1].nesting, 2);
assert_eq!(decoded.contributions[1].metric, ComplexityMetric::Cognitive);
}
}