1use oxc_span::Span;
4
5use crate::discover::FileId;
6use crate::suppress::Suppression;
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 member_accesses: Vec<MemberAccess>,
27 pub whole_object_uses: Vec<String>,
30 pub has_cjs_exports: bool,
32 pub content_hash: u64,
34 pub suppressions: Vec<Suppression>,
36 pub unused_import_bindings: Vec<String>,
41 pub line_offsets: Vec<u32>,
45 pub complexity: Vec<FunctionComplexity>,
48}
49
50#[must_use]
64#[expect(
65 clippy::cast_possible_truncation,
66 reason = "source files are practically < 4GB"
67)]
68pub fn compute_line_offsets(source: &str) -> Vec<u32> {
69 let mut offsets = vec![0u32];
70 for (i, byte) in source.bytes().enumerate() {
71 if byte == b'\n' {
72 debug_assert!(
73 u32::try_from(i + 1).is_ok(),
74 "source file exceeds u32::MAX bytes — line offsets would overflow"
75 );
76 offsets.push((i + 1) as u32);
77 }
78 }
79 offsets
80}
81
82#[must_use]
98#[expect(
99 clippy::cast_possible_truncation,
100 reason = "line count is bounded by source size"
101)]
102pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
103 let line_idx = match line_offsets.binary_search(&byte_offset) {
105 Ok(idx) => idx,
106 Err(idx) => idx.saturating_sub(1),
107 };
108 let line = line_idx as u32 + 1; let col = byte_offset - line_offsets[line_idx];
110 (line, col)
111}
112
113#[derive(Debug, Clone, serde::Serialize, bincode::Encode, bincode::Decode)]
115pub struct FunctionComplexity {
116 pub name: String,
118 pub line: u32,
120 pub col: u32,
122 pub cyclomatic: u16,
124 pub cognitive: u16,
126 pub line_count: u32,
128}
129
130#[derive(Debug, Clone)]
132pub struct DynamicImportPattern {
133 pub prefix: String,
135 pub suffix: Option<String>,
137 pub span: Span,
139}
140
141#[derive(Debug, Clone, serde::Serialize)]
143pub struct ExportInfo {
144 pub name: ExportName,
146 pub local_name: Option<String>,
148 pub is_type_only: bool,
150 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
154 pub is_public: bool,
155 #[serde(serialize_with = "serialize_span")]
157 pub span: Span,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub members: Vec<MemberInfo>,
161}
162
163#[derive(Debug, Clone, serde::Serialize)]
165pub struct MemberInfo {
166 pub name: String,
168 pub kind: MemberKind,
170 #[serde(serialize_with = "serialize_span")]
172 pub span: Span,
173 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
177 pub has_decorator: bool,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
193#[serde(rename_all = "snake_case")]
194pub enum MemberKind {
195 EnumMember,
197 ClassMethod,
199 ClassProperty,
201 NamespaceMember,
203}
204
205#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
220pub struct MemberAccess {
221 pub object: String,
223 pub member: String,
225}
226
227#[expect(
228 clippy::trivially_copy_pass_by_ref,
229 reason = "serde serialize_with requires &T"
230)]
231fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
232 use serde::ser::SerializeMap;
233 let mut map = serializer.serialize_map(Some(2))?;
234 map.serialize_entry("start", &span.start)?;
235 map.serialize_entry("end", &span.end)?;
236 map.end()
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
255pub enum ExportName {
256 Named(String),
258 Default,
260}
261
262impl ExportName {
263 #[must_use]
265 pub fn matches_str(&self, s: &str) -> bool {
266 match self {
267 Self::Named(n) => n == s,
268 Self::Default => s == "default",
269 }
270 }
271}
272
273impl std::fmt::Display for ExportName {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 match self {
276 Self::Named(n) => write!(f, "{n}"),
277 Self::Default => write!(f, "default"),
278 }
279 }
280}
281
282#[derive(Debug, Clone)]
284pub struct ImportInfo {
285 pub source: String,
287 pub imported_name: ImportedName,
289 pub local_name: String,
291 pub is_type_only: bool,
293 pub span: Span,
295 pub source_span: Span,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
315pub enum ImportedName {
316 Named(String),
318 Default,
320 Namespace,
322 SideEffect,
324}
325
326#[cfg(target_pointer_width = "64")]
331const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
332#[cfg(target_pointer_width = "64")]
333const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
334#[cfg(target_pointer_width = "64")]
335const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
336#[cfg(target_pointer_width = "64")]
337const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
338#[cfg(target_pointer_width = "64")]
339const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
340#[cfg(target_pointer_width = "64")]
342const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 304);
343
344#[derive(Debug, Clone)]
346pub struct ReExportInfo {
347 pub source: String,
349 pub imported_name: String,
351 pub exported_name: String,
353 pub is_type_only: bool,
355}
356
357#[derive(Debug, Clone)]
359pub struct DynamicImportInfo {
360 pub source: String,
362 pub span: Span,
364 pub destructured_names: Vec<String>,
368 pub local_name: Option<String>,
371}
372
373#[derive(Debug, Clone)]
375pub struct RequireCallInfo {
376 pub source: String,
378 pub span: Span,
380 pub destructured_names: Vec<String>,
384 pub local_name: Option<String>,
387}
388
389pub struct ParseResult {
391 pub modules: Vec<ModuleInfo>,
393 pub cache_hits: usize,
395 pub cache_misses: usize,
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
406 fn line_offsets_empty_string() {
407 assert_eq!(compute_line_offsets(""), vec![0]);
408 }
409
410 #[test]
411 fn line_offsets_single_line_no_newline() {
412 assert_eq!(compute_line_offsets("hello"), vec![0]);
413 }
414
415 #[test]
416 fn line_offsets_single_line_with_newline() {
417 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
419 }
420
421 #[test]
422 fn line_offsets_multiple_lines() {
423 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
428 }
429
430 #[test]
431 fn line_offsets_trailing_newline() {
432 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
435 }
436
437 #[test]
438 fn line_offsets_consecutive_newlines() {
439 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
441 }
442
443 #[test]
444 fn line_offsets_multibyte_utf8() {
445 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
447 }
448
449 #[test]
452 fn line_col_offset_zero() {
453 let offsets = compute_line_offsets("abc\ndef\nghi");
454 let (line, col) = byte_offset_to_line_col(&offsets, 0);
455 assert_eq!((line, col), (1, 0)); }
457
458 #[test]
459 fn line_col_middle_of_first_line() {
460 let offsets = compute_line_offsets("abc\ndef\nghi");
461 let (line, col) = byte_offset_to_line_col(&offsets, 2);
462 assert_eq!((line, col), (1, 2)); }
464
465 #[test]
466 fn line_col_start_of_second_line() {
467 let offsets = compute_line_offsets("abc\ndef\nghi");
468 let (line, col) = byte_offset_to_line_col(&offsets, 4);
470 assert_eq!((line, col), (2, 0));
471 }
472
473 #[test]
474 fn line_col_middle_of_second_line() {
475 let offsets = compute_line_offsets("abc\ndef\nghi");
476 let (line, col) = byte_offset_to_line_col(&offsets, 5);
478 assert_eq!((line, col), (2, 1));
479 }
480
481 #[test]
482 fn line_col_start_of_third_line() {
483 let offsets = compute_line_offsets("abc\ndef\nghi");
484 let (line, col) = byte_offset_to_line_col(&offsets, 8);
486 assert_eq!((line, col), (3, 0));
487 }
488
489 #[test]
490 fn line_col_end_of_file() {
491 let offsets = compute_line_offsets("abc\ndef\nghi");
492 let (line, col) = byte_offset_to_line_col(&offsets, 10);
494 assert_eq!((line, col), (3, 2));
495 }
496
497 #[test]
498 fn line_col_single_line() {
499 let offsets = compute_line_offsets("hello");
500 let (line, col) = byte_offset_to_line_col(&offsets, 3);
501 assert_eq!((line, col), (1, 3));
502 }
503
504 #[test]
505 fn line_col_at_newline_byte() {
506 let offsets = compute_line_offsets("abc\ndef");
507 let (line, col) = byte_offset_to_line_col(&offsets, 3);
509 assert_eq!((line, col), (1, 3));
510 }
511
512 #[test]
515 fn export_name_matches_str_named() {
516 let name = ExportName::Named("foo".to_string());
517 assert!(name.matches_str("foo"));
518 assert!(!name.matches_str("bar"));
519 assert!(!name.matches_str("default"));
520 }
521
522 #[test]
523 fn export_name_matches_str_default() {
524 let name = ExportName::Default;
525 assert!(name.matches_str("default"));
526 assert!(!name.matches_str("foo"));
527 }
528
529 #[test]
530 fn export_name_display_named() {
531 let name = ExportName::Named("myExport".to_string());
532 assert_eq!(name.to_string(), "myExport");
533 }
534
535 #[test]
536 fn export_name_display_default() {
537 let name = ExportName::Default;
538 assert_eq!(name.to_string(), "default");
539 }
540
541 #[test]
544 fn export_name_equality_named() {
545 let a = ExportName::Named("foo".to_string());
546 let b = ExportName::Named("foo".to_string());
547 let c = ExportName::Named("bar".to_string());
548 assert_eq!(a, b);
549 assert_ne!(a, c);
550 }
551
552 #[test]
553 fn export_name_equality_default() {
554 let a = ExportName::Default;
555 let b = ExportName::Default;
556 assert_eq!(a, b);
557 }
558
559 #[test]
560 fn export_name_named_not_equal_to_default() {
561 let named = ExportName::Named("default".to_string());
562 let default = ExportName::Default;
563 assert_ne!(named, default);
564 }
565
566 #[test]
567 fn export_name_hash_consistency() {
568 use std::collections::hash_map::DefaultHasher;
569 use std::hash::{Hash, Hasher};
570
571 let mut h1 = DefaultHasher::new();
572 let mut h2 = DefaultHasher::new();
573 ExportName::Named("foo".to_string()).hash(&mut h1);
574 ExportName::Named("foo".to_string()).hash(&mut h2);
575 assert_eq!(h1.finish(), h2.finish());
576 }
577
578 #[test]
581 fn export_name_matches_str_empty_string() {
582 let name = ExportName::Named(String::new());
583 assert!(name.matches_str(""));
584 assert!(!name.matches_str("foo"));
585 }
586
587 #[test]
588 fn export_name_default_does_not_match_empty() {
589 let name = ExportName::Default;
590 assert!(!name.matches_str(""));
591 }
592
593 #[test]
596 fn imported_name_equality() {
597 assert_eq!(
598 ImportedName::Named("foo".to_string()),
599 ImportedName::Named("foo".to_string())
600 );
601 assert_ne!(
602 ImportedName::Named("foo".to_string()),
603 ImportedName::Named("bar".to_string())
604 );
605 assert_eq!(ImportedName::Default, ImportedName::Default);
606 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
607 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
608 assert_ne!(ImportedName::Default, ImportedName::Namespace);
609 assert_ne!(
610 ImportedName::Named("default".to_string()),
611 ImportedName::Default
612 );
613 }
614
615 #[test]
618 fn member_kind_equality() {
619 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
620 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
621 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
622 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
623 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
624 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
625 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
626 }
627
628 #[test]
631 fn member_kind_bincode_roundtrip() {
632 let kinds = [
633 MemberKind::EnumMember,
634 MemberKind::ClassMethod,
635 MemberKind::ClassProperty,
636 MemberKind::NamespaceMember,
637 ];
638 let config = bincode::config::standard();
639 for kind in &kinds {
640 let bytes = bincode::encode_to_vec(kind, config).unwrap();
641 let (decoded, _): (MemberKind, _) = bincode::decode_from_slice(&bytes, config).unwrap();
642 assert_eq!(&decoded, kind);
643 }
644 }
645
646 #[test]
649 fn member_access_bincode_roundtrip() {
650 let access = MemberAccess {
651 object: "Status".to_string(),
652 member: "Active".to_string(),
653 };
654 let config = bincode::config::standard();
655 let bytes = bincode::encode_to_vec(&access, config).unwrap();
656 let (decoded, _): (MemberAccess, _) = bincode::decode_from_slice(&bytes, config).unwrap();
657 assert_eq!(decoded.object, "Status");
658 assert_eq!(decoded.member, "Active");
659 }
660
661 #[test]
664 fn line_offsets_crlf_only_counts_lf() {
665 let offsets = compute_line_offsets("ab\r\ncd");
669 assert_eq!(offsets, vec![0, 4]);
670 }
671
672 #[test]
675 fn line_col_empty_file_offset_zero() {
676 let offsets = compute_line_offsets("");
677 let (line, col) = byte_offset_to_line_col(&offsets, 0);
678 assert_eq!((line, col), (1, 0));
679 }
680
681 #[test]
684 fn function_complexity_bincode_roundtrip() {
685 let fc = FunctionComplexity {
686 name: "processData".to_string(),
687 line: 42,
688 col: 4,
689 cyclomatic: 15,
690 cognitive: 25,
691 line_count: 80,
692 };
693 let config = bincode::config::standard();
694 let bytes = bincode::encode_to_vec(&fc, config).unwrap();
695 let (decoded, _): (FunctionComplexity, _) =
696 bincode::decode_from_slice(&bytes, config).unwrap();
697 assert_eq!(decoded.name, "processData");
698 assert_eq!(decoded.line, 42);
699 assert_eq!(decoded.col, 4);
700 assert_eq!(decoded.cyclomatic, 15);
701 assert_eq!(decoded.cognitive, 25);
702 assert_eq!(decoded.line_count, 80);
703 }
704}