use oxc_span::Span;
use crate::discover::FileId;
use crate::suppress::Suppression;
#[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 member_accesses: Vec<MemberAccess>,
pub whole_object_uses: Vec<String>,
pub has_cjs_exports: bool,
pub content_hash: u64,
pub suppressions: Vec<Suppression>,
pub unused_import_bindings: Vec<String>,
pub line_offsets: Vec<u32>,
pub complexity: Vec<FunctionComplexity>,
pub flag_uses: Vec<FlagUse>,
}
#[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,
}
#[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 = "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)]
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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
#[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 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::<ModuleInfo>() == 328);
#[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>,
}
#[derive(Debug, Clone)]
pub struct RequireCallInfo {
pub source: String,
pub 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,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_offsets_empty_string() {
assert_eq!(compute_line_offsets(""), vec![0]);
}
#[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,
};
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);
}
}