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>,
}
#[must_use]
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' {
offsets.push((i + 1) as u32);
}
}
offsets
}
#[must_use]
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, bincode::Encode, bincode::Decode)]
pub struct FunctionComplexity {
pub name: String,
pub line: u32,
pub col: u32,
pub cyclomatic: u16,
pub cognitive: u16,
pub line_count: u32,
}
#[derive(Debug, Clone)]
pub struct DynamicImportPattern {
pub prefix: String,
pub suffix: Option<String>,
pub span: Span,
}
#[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_public: bool,
#[serde(serialize_with = "serialize_span")]
pub span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub members: Vec<MemberInfo>,
}
#[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, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
#[serde(rename_all = "snake_case")]
pub enum MemberKind {
EnumMember,
ClassMethod,
ClassProperty,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
pub struct MemberAccess {
pub object: String,
pub member: String,
}
#[expect(clippy::trivially_copy_pass_by_ref)] 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>() == 88);
#[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>() == 304);
#[derive(Debug, Clone)]
pub struct ReExportInfo {
pub source: String,
pub imported_name: String,
pub exported_name: String,
pub is_type_only: bool,
}
#[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");
}
}