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, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
193#[serde(rename_all = "snake_case")]
194pub enum MemberKind {
195 EnumMember,
197 ClassMethod,
199 ClassProperty,
201}
202
203#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
218pub struct MemberAccess {
219 pub object: String,
221 pub member: String,
223}
224
225#[expect(clippy::trivially_copy_pass_by_ref)] fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
227 use serde::ser::SerializeMap;
228 let mut map = serializer.serialize_map(Some(2))?;
229 map.serialize_entry("start", &span.start)?;
230 map.serialize_entry("end", &span.end)?;
231 map.end()
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
250pub enum ExportName {
251 Named(String),
253 Default,
255}
256
257impl ExportName {
258 #[must_use]
260 pub fn matches_str(&self, s: &str) -> bool {
261 match self {
262 Self::Named(n) => n == s,
263 Self::Default => s == "default",
264 }
265 }
266}
267
268impl std::fmt::Display for ExportName {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 match self {
271 Self::Named(n) => write!(f, "{n}"),
272 Self::Default => write!(f, "default"),
273 }
274 }
275}
276
277#[derive(Debug, Clone)]
279pub struct ImportInfo {
280 pub source: String,
282 pub imported_name: ImportedName,
284 pub local_name: String,
286 pub is_type_only: bool,
288 pub span: Span,
290 pub source_span: Span,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum ImportedName {
311 Named(String),
313 Default,
315 Namespace,
317 SideEffect,
319}
320
321#[cfg(target_pointer_width = "64")]
326const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
327#[cfg(target_pointer_width = "64")]
328const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
329#[cfg(target_pointer_width = "64")]
330const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
331#[cfg(target_pointer_width = "64")]
332const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
333#[cfg(target_pointer_width = "64")]
334const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
335#[cfg(target_pointer_width = "64")]
337const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 304);
338
339#[derive(Debug, Clone)]
341pub struct ReExportInfo {
342 pub source: String,
344 pub imported_name: String,
346 pub exported_name: String,
348 pub is_type_only: bool,
350}
351
352#[derive(Debug, Clone)]
354pub struct DynamicImportInfo {
355 pub source: String,
357 pub span: Span,
359 pub destructured_names: Vec<String>,
363 pub local_name: Option<String>,
366}
367
368#[derive(Debug, Clone)]
370pub struct RequireCallInfo {
371 pub source: String,
373 pub span: Span,
375 pub destructured_names: Vec<String>,
379 pub local_name: Option<String>,
382}
383
384pub struct ParseResult {
386 pub modules: Vec<ModuleInfo>,
388 pub cache_hits: usize,
390 pub cache_misses: usize,
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
401 fn line_offsets_empty_string() {
402 assert_eq!(compute_line_offsets(""), vec![0]);
403 }
404
405 #[test]
406 fn line_offsets_single_line_no_newline() {
407 assert_eq!(compute_line_offsets("hello"), vec![0]);
408 }
409
410 #[test]
411 fn line_offsets_single_line_with_newline() {
412 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
414 }
415
416 #[test]
417 fn line_offsets_multiple_lines() {
418 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
423 }
424
425 #[test]
426 fn line_offsets_trailing_newline() {
427 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
430 }
431
432 #[test]
433 fn line_offsets_consecutive_newlines() {
434 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
436 }
437
438 #[test]
439 fn line_offsets_multibyte_utf8() {
440 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
442 }
443
444 #[test]
447 fn line_col_offset_zero() {
448 let offsets = compute_line_offsets("abc\ndef\nghi");
449 let (line, col) = byte_offset_to_line_col(&offsets, 0);
450 assert_eq!((line, col), (1, 0)); }
452
453 #[test]
454 fn line_col_middle_of_first_line() {
455 let offsets = compute_line_offsets("abc\ndef\nghi");
456 let (line, col) = byte_offset_to_line_col(&offsets, 2);
457 assert_eq!((line, col), (1, 2)); }
459
460 #[test]
461 fn line_col_start_of_second_line() {
462 let offsets = compute_line_offsets("abc\ndef\nghi");
463 let (line, col) = byte_offset_to_line_col(&offsets, 4);
465 assert_eq!((line, col), (2, 0));
466 }
467
468 #[test]
469 fn line_col_middle_of_second_line() {
470 let offsets = compute_line_offsets("abc\ndef\nghi");
471 let (line, col) = byte_offset_to_line_col(&offsets, 5);
473 assert_eq!((line, col), (2, 1));
474 }
475
476 #[test]
477 fn line_col_start_of_third_line() {
478 let offsets = compute_line_offsets("abc\ndef\nghi");
479 let (line, col) = byte_offset_to_line_col(&offsets, 8);
481 assert_eq!((line, col), (3, 0));
482 }
483
484 #[test]
485 fn line_col_end_of_file() {
486 let offsets = compute_line_offsets("abc\ndef\nghi");
487 let (line, col) = byte_offset_to_line_col(&offsets, 10);
489 assert_eq!((line, col), (3, 2));
490 }
491
492 #[test]
493 fn line_col_single_line() {
494 let offsets = compute_line_offsets("hello");
495 let (line, col) = byte_offset_to_line_col(&offsets, 3);
496 assert_eq!((line, col), (1, 3));
497 }
498
499 #[test]
500 fn line_col_at_newline_byte() {
501 let offsets = compute_line_offsets("abc\ndef");
502 let (line, col) = byte_offset_to_line_col(&offsets, 3);
504 assert_eq!((line, col), (1, 3));
505 }
506
507 #[test]
510 fn export_name_matches_str_named() {
511 let name = ExportName::Named("foo".to_string());
512 assert!(name.matches_str("foo"));
513 assert!(!name.matches_str("bar"));
514 assert!(!name.matches_str("default"));
515 }
516
517 #[test]
518 fn export_name_matches_str_default() {
519 let name = ExportName::Default;
520 assert!(name.matches_str("default"));
521 assert!(!name.matches_str("foo"));
522 }
523
524 #[test]
525 fn export_name_display_named() {
526 let name = ExportName::Named("myExport".to_string());
527 assert_eq!(name.to_string(), "myExport");
528 }
529
530 #[test]
531 fn export_name_display_default() {
532 let name = ExportName::Default;
533 assert_eq!(name.to_string(), "default");
534 }
535
536 #[test]
539 fn export_name_equality_named() {
540 let a = ExportName::Named("foo".to_string());
541 let b = ExportName::Named("foo".to_string());
542 let c = ExportName::Named("bar".to_string());
543 assert_eq!(a, b);
544 assert_ne!(a, c);
545 }
546
547 #[test]
548 fn export_name_equality_default() {
549 let a = ExportName::Default;
550 let b = ExportName::Default;
551 assert_eq!(a, b);
552 }
553
554 #[test]
555 fn export_name_named_not_equal_to_default() {
556 let named = ExportName::Named("default".to_string());
557 let default = ExportName::Default;
558 assert_ne!(named, default);
559 }
560
561 #[test]
562 fn export_name_hash_consistency() {
563 use std::collections::hash_map::DefaultHasher;
564 use std::hash::{Hash, Hasher};
565
566 let mut h1 = DefaultHasher::new();
567 let mut h2 = DefaultHasher::new();
568 ExportName::Named("foo".to_string()).hash(&mut h1);
569 ExportName::Named("foo".to_string()).hash(&mut h2);
570 assert_eq!(h1.finish(), h2.finish());
571 }
572
573 #[test]
576 fn export_name_matches_str_empty_string() {
577 let name = ExportName::Named(String::new());
578 assert!(name.matches_str(""));
579 assert!(!name.matches_str("foo"));
580 }
581
582 #[test]
583 fn export_name_default_does_not_match_empty() {
584 let name = ExportName::Default;
585 assert!(!name.matches_str(""));
586 }
587
588 #[test]
591 fn imported_name_equality() {
592 assert_eq!(
593 ImportedName::Named("foo".to_string()),
594 ImportedName::Named("foo".to_string())
595 );
596 assert_ne!(
597 ImportedName::Named("foo".to_string()),
598 ImportedName::Named("bar".to_string())
599 );
600 assert_eq!(ImportedName::Default, ImportedName::Default);
601 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
602 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
603 assert_ne!(ImportedName::Default, ImportedName::Namespace);
604 assert_ne!(
605 ImportedName::Named("default".to_string()),
606 ImportedName::Default
607 );
608 }
609
610 #[test]
613 fn member_kind_equality() {
614 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
615 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
616 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
617 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
618 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
619 }
620
621 #[test]
624 fn member_kind_bincode_roundtrip() {
625 let kinds = [
626 MemberKind::EnumMember,
627 MemberKind::ClassMethod,
628 MemberKind::ClassProperty,
629 ];
630 let config = bincode::config::standard();
631 for kind in &kinds {
632 let bytes = bincode::encode_to_vec(kind, config).unwrap();
633 let (decoded, _): (MemberKind, _) = bincode::decode_from_slice(&bytes, config).unwrap();
634 assert_eq!(&decoded, kind);
635 }
636 }
637
638 #[test]
641 fn member_access_bincode_roundtrip() {
642 let access = MemberAccess {
643 object: "Status".to_string(),
644 member: "Active".to_string(),
645 };
646 let config = bincode::config::standard();
647 let bytes = bincode::encode_to_vec(&access, config).unwrap();
648 let (decoded, _): (MemberAccess, _) = bincode::decode_from_slice(&bytes, config).unwrap();
649 assert_eq!(decoded.object, "Status");
650 assert_eq!(decoded.member, "Active");
651 }
652
653 #[test]
656 fn line_offsets_crlf_only_counts_lf() {
657 let offsets = compute_line_offsets("ab\r\ncd");
661 assert_eq!(offsets, vec![0, 4]);
662 }
663
664 #[test]
667 fn line_col_empty_file_offset_zero() {
668 let offsets = compute_line_offsets("");
669 let (line, col) = byte_offset_to_line_col(&offsets, 0);
670 assert_eq!((line, col), (1, 0));
671 }
672
673 #[test]
676 fn function_complexity_bincode_roundtrip() {
677 let fc = FunctionComplexity {
678 name: "processData".to_string(),
679 line: 42,
680 col: 4,
681 cyclomatic: 15,
682 cognitive: 25,
683 line_count: 80,
684 };
685 let config = bincode::config::standard();
686 let bytes = bincode::encode_to_vec(&fc, config).unwrap();
687 let (decoded, _): (FunctionComplexity, _) =
688 bincode::decode_from_slice(&bytes, config).unwrap();
689 assert_eq!(decoded.name, "processData");
690 assert_eq!(decoded.line, 42);
691 assert_eq!(decoded.col, 4);
692 assert_eq!(decoded.cyclomatic, 15);
693 assert_eq!(decoded.cognitive, 25);
694 assert_eq!(decoded.line_count, 80);
695 }
696}