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, bitcode::Encode, bitcode::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 pub param_count: u8,
130}
131
132#[derive(Debug, Clone)]
134pub struct DynamicImportPattern {
135 pub prefix: String,
137 pub suffix: Option<String>,
139 pub span: Span,
141}
142
143#[derive(Debug, Clone, serde::Serialize)]
145pub struct ExportInfo {
146 pub name: ExportName,
148 pub local_name: Option<String>,
150 pub is_type_only: bool,
152 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
156 pub is_public: bool,
157 #[serde(serialize_with = "serialize_span")]
159 pub span: Span,
160 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub members: Vec<MemberInfo>,
163}
164
165#[derive(Debug, Clone, serde::Serialize)]
167pub struct MemberInfo {
168 pub name: String,
170 pub kind: MemberKind,
172 #[serde(serialize_with = "serialize_span")]
174 pub span: Span,
175 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
179 pub has_decorator: bool,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, bitcode::Encode, bitcode::Decode)]
195#[serde(rename_all = "snake_case")]
196pub enum MemberKind {
197 EnumMember,
199 ClassMethod,
201 ClassProperty,
203 NamespaceMember,
205}
206
207#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bitcode::Encode, bitcode::Decode)]
222pub struct MemberAccess {
223 pub object: String,
225 pub member: String,
227}
228
229#[expect(
230 clippy::trivially_copy_pass_by_ref,
231 reason = "serde serialize_with requires &T"
232)]
233fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
234 use serde::ser::SerializeMap;
235 let mut map = serializer.serialize_map(Some(2))?;
236 map.serialize_entry("start", &span.start)?;
237 map.serialize_entry("end", &span.end)?;
238 map.end()
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
257pub enum ExportName {
258 Named(String),
260 Default,
262}
263
264impl ExportName {
265 #[must_use]
267 pub fn matches_str(&self, s: &str) -> bool {
268 match self {
269 Self::Named(n) => n == s,
270 Self::Default => s == "default",
271 }
272 }
273}
274
275impl std::fmt::Display for ExportName {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 match self {
278 Self::Named(n) => write!(f, "{n}"),
279 Self::Default => write!(f, "default"),
280 }
281 }
282}
283
284#[derive(Debug, Clone)]
286pub struct ImportInfo {
287 pub source: String,
289 pub imported_name: ImportedName,
291 pub local_name: String,
293 pub is_type_only: bool,
295 pub span: Span,
297 pub source_span: Span,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
317pub enum ImportedName {
318 Named(String),
320 Default,
322 Namespace,
324 SideEffect,
326}
327
328#[cfg(target_pointer_width = "64")]
333const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
334#[cfg(target_pointer_width = "64")]
335const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
336#[cfg(target_pointer_width = "64")]
337const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
338#[cfg(target_pointer_width = "64")]
339const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
340#[cfg(target_pointer_width = "64")]
341const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
342#[cfg(target_pointer_width = "64")]
344const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 304);
345
346#[derive(Debug, Clone)]
348pub struct ReExportInfo {
349 pub source: String,
351 pub imported_name: String,
353 pub exported_name: String,
355 pub is_type_only: bool,
357}
358
359#[derive(Debug, Clone)]
361pub struct DynamicImportInfo {
362 pub source: String,
364 pub span: Span,
366 pub destructured_names: Vec<String>,
370 pub local_name: Option<String>,
373}
374
375#[derive(Debug, Clone)]
377pub struct RequireCallInfo {
378 pub source: String,
380 pub span: Span,
382 pub destructured_names: Vec<String>,
386 pub local_name: Option<String>,
389}
390
391pub struct ParseResult {
393 pub modules: Vec<ModuleInfo>,
395 pub cache_hits: usize,
397 pub cache_misses: usize,
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
408 fn line_offsets_empty_string() {
409 assert_eq!(compute_line_offsets(""), vec![0]);
410 }
411
412 #[test]
413 fn line_offsets_single_line_no_newline() {
414 assert_eq!(compute_line_offsets("hello"), vec![0]);
415 }
416
417 #[test]
418 fn line_offsets_single_line_with_newline() {
419 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
421 }
422
423 #[test]
424 fn line_offsets_multiple_lines() {
425 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
430 }
431
432 #[test]
433 fn line_offsets_trailing_newline() {
434 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
437 }
438
439 #[test]
440 fn line_offsets_consecutive_newlines() {
441 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
443 }
444
445 #[test]
446 fn line_offsets_multibyte_utf8() {
447 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
449 }
450
451 #[test]
454 fn line_col_offset_zero() {
455 let offsets = compute_line_offsets("abc\ndef\nghi");
456 let (line, col) = byte_offset_to_line_col(&offsets, 0);
457 assert_eq!((line, col), (1, 0)); }
459
460 #[test]
461 fn line_col_middle_of_first_line() {
462 let offsets = compute_line_offsets("abc\ndef\nghi");
463 let (line, col) = byte_offset_to_line_col(&offsets, 2);
464 assert_eq!((line, col), (1, 2)); }
466
467 #[test]
468 fn line_col_start_of_second_line() {
469 let offsets = compute_line_offsets("abc\ndef\nghi");
470 let (line, col) = byte_offset_to_line_col(&offsets, 4);
472 assert_eq!((line, col), (2, 0));
473 }
474
475 #[test]
476 fn line_col_middle_of_second_line() {
477 let offsets = compute_line_offsets("abc\ndef\nghi");
478 let (line, col) = byte_offset_to_line_col(&offsets, 5);
480 assert_eq!((line, col), (2, 1));
481 }
482
483 #[test]
484 fn line_col_start_of_third_line() {
485 let offsets = compute_line_offsets("abc\ndef\nghi");
486 let (line, col) = byte_offset_to_line_col(&offsets, 8);
488 assert_eq!((line, col), (3, 0));
489 }
490
491 #[test]
492 fn line_col_end_of_file() {
493 let offsets = compute_line_offsets("abc\ndef\nghi");
494 let (line, col) = byte_offset_to_line_col(&offsets, 10);
496 assert_eq!((line, col), (3, 2));
497 }
498
499 #[test]
500 fn line_col_single_line() {
501 let offsets = compute_line_offsets("hello");
502 let (line, col) = byte_offset_to_line_col(&offsets, 3);
503 assert_eq!((line, col), (1, 3));
504 }
505
506 #[test]
507 fn line_col_at_newline_byte() {
508 let offsets = compute_line_offsets("abc\ndef");
509 let (line, col) = byte_offset_to_line_col(&offsets, 3);
511 assert_eq!((line, col), (1, 3));
512 }
513
514 #[test]
517 fn export_name_matches_str_named() {
518 let name = ExportName::Named("foo".to_string());
519 assert!(name.matches_str("foo"));
520 assert!(!name.matches_str("bar"));
521 assert!(!name.matches_str("default"));
522 }
523
524 #[test]
525 fn export_name_matches_str_default() {
526 let name = ExportName::Default;
527 assert!(name.matches_str("default"));
528 assert!(!name.matches_str("foo"));
529 }
530
531 #[test]
532 fn export_name_display_named() {
533 let name = ExportName::Named("myExport".to_string());
534 assert_eq!(name.to_string(), "myExport");
535 }
536
537 #[test]
538 fn export_name_display_default() {
539 let name = ExportName::Default;
540 assert_eq!(name.to_string(), "default");
541 }
542
543 #[test]
546 fn export_name_equality_named() {
547 let a = ExportName::Named("foo".to_string());
548 let b = ExportName::Named("foo".to_string());
549 let c = ExportName::Named("bar".to_string());
550 assert_eq!(a, b);
551 assert_ne!(a, c);
552 }
553
554 #[test]
555 fn export_name_equality_default() {
556 let a = ExportName::Default;
557 let b = ExportName::Default;
558 assert_eq!(a, b);
559 }
560
561 #[test]
562 fn export_name_named_not_equal_to_default() {
563 let named = ExportName::Named("default".to_string());
564 let default = ExportName::Default;
565 assert_ne!(named, default);
566 }
567
568 #[test]
569 fn export_name_hash_consistency() {
570 use std::collections::hash_map::DefaultHasher;
571 use std::hash::{Hash, Hasher};
572
573 let mut h1 = DefaultHasher::new();
574 let mut h2 = DefaultHasher::new();
575 ExportName::Named("foo".to_string()).hash(&mut h1);
576 ExportName::Named("foo".to_string()).hash(&mut h2);
577 assert_eq!(h1.finish(), h2.finish());
578 }
579
580 #[test]
583 fn export_name_matches_str_empty_string() {
584 let name = ExportName::Named(String::new());
585 assert!(name.matches_str(""));
586 assert!(!name.matches_str("foo"));
587 }
588
589 #[test]
590 fn export_name_default_does_not_match_empty() {
591 let name = ExportName::Default;
592 assert!(!name.matches_str(""));
593 }
594
595 #[test]
598 fn imported_name_equality() {
599 assert_eq!(
600 ImportedName::Named("foo".to_string()),
601 ImportedName::Named("foo".to_string())
602 );
603 assert_ne!(
604 ImportedName::Named("foo".to_string()),
605 ImportedName::Named("bar".to_string())
606 );
607 assert_eq!(ImportedName::Default, ImportedName::Default);
608 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
609 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
610 assert_ne!(ImportedName::Default, ImportedName::Namespace);
611 assert_ne!(
612 ImportedName::Named("default".to_string()),
613 ImportedName::Default
614 );
615 }
616
617 #[test]
620 fn member_kind_equality() {
621 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
622 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
623 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
624 assert_eq!(MemberKind::NamespaceMember, MemberKind::NamespaceMember);
625 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
626 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
627 assert_ne!(MemberKind::NamespaceMember, MemberKind::EnumMember);
628 }
629
630 #[test]
633 fn member_kind_bitcode_roundtrip() {
634 let kinds = [
635 MemberKind::EnumMember,
636 MemberKind::ClassMethod,
637 MemberKind::ClassProperty,
638 MemberKind::NamespaceMember,
639 ];
640 for kind in &kinds {
641 let bytes = bitcode::encode(kind);
642 let decoded: MemberKind = bitcode::decode(&bytes).unwrap();
643 assert_eq!(&decoded, kind);
644 }
645 }
646
647 #[test]
650 fn member_access_bitcode_roundtrip() {
651 let access = MemberAccess {
652 object: "Status".to_string(),
653 member: "Active".to_string(),
654 };
655 let bytes = bitcode::encode(&access);
656 let decoded: MemberAccess = bitcode::decode(&bytes).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_bitcode_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 param_count: 3,
693 };
694 let bytes = bitcode::encode(&fc);
695 let decoded: FunctionComplexity = bitcode::decode(&bytes).unwrap();
696 assert_eq!(decoded.name, "processData");
697 assert_eq!(decoded.line, 42);
698 assert_eq!(decoded.col, 4);
699 assert_eq!(decoded.cyclomatic, 15);
700 assert_eq!(decoded.cognitive, 25);
701 assert_eq!(decoded.line_count, 80);
702 }
703}