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]
64pub fn compute_line_offsets(source: &str) -> Vec<u32> {
65 let mut offsets = vec![0u32];
66 for (i, byte) in source.bytes().enumerate() {
67 if byte == b'\n' {
68 debug_assert!(
69 u32::try_from(i + 1).is_ok(),
70 "source file exceeds u32::MAX bytes — line offsets would overflow"
71 );
72 offsets.push((i + 1) as u32);
73 }
74 }
75 offsets
76}
77
78#[must_use]
94pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
95 let line_idx = match line_offsets.binary_search(&byte_offset) {
97 Ok(idx) => idx,
98 Err(idx) => idx.saturating_sub(1),
99 };
100 let line = line_idx as u32 + 1; let col = byte_offset - line_offsets[line_idx];
102 (line, col)
103}
104
105#[derive(Debug, Clone, serde::Serialize, bincode::Encode, bincode::Decode)]
107pub struct FunctionComplexity {
108 pub name: String,
110 pub line: u32,
112 pub col: u32,
114 pub cyclomatic: u16,
116 pub cognitive: u16,
118 pub line_count: u32,
120}
121
122#[derive(Debug, Clone)]
124pub struct DynamicImportPattern {
125 pub prefix: String,
127 pub suffix: Option<String>,
129 pub span: Span,
131}
132
133#[derive(Debug, Clone, serde::Serialize)]
135pub struct ExportInfo {
136 pub name: ExportName,
138 pub local_name: Option<String>,
140 pub is_type_only: bool,
142 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
146 pub is_public: bool,
147 #[serde(serialize_with = "serialize_span")]
149 pub span: Span,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub members: Vec<MemberInfo>,
153}
154
155#[derive(Debug, Clone, serde::Serialize)]
157pub struct MemberInfo {
158 pub name: String,
160 pub kind: MemberKind,
162 #[serde(serialize_with = "serialize_span")]
164 pub span: Span,
165 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
169 pub has_decorator: bool,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
185#[serde(rename_all = "snake_case")]
186pub enum MemberKind {
187 EnumMember,
189 ClassMethod,
191 ClassProperty,
193}
194
195#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
210pub struct MemberAccess {
211 pub object: String,
213 pub member: String,
215}
216
217#[expect(clippy::trivially_copy_pass_by_ref)] fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
219 use serde::ser::SerializeMap;
220 let mut map = serializer.serialize_map(Some(2))?;
221 map.serialize_entry("start", &span.start)?;
222 map.serialize_entry("end", &span.end)?;
223 map.end()
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
242pub enum ExportName {
243 Named(String),
245 Default,
247}
248
249impl ExportName {
250 #[must_use]
252 pub fn matches_str(&self, s: &str) -> bool {
253 match self {
254 Self::Named(n) => n == s,
255 Self::Default => s == "default",
256 }
257 }
258}
259
260impl std::fmt::Display for ExportName {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 match self {
263 Self::Named(n) => write!(f, "{n}"),
264 Self::Default => write!(f, "default"),
265 }
266 }
267}
268
269#[derive(Debug, Clone)]
271pub struct ImportInfo {
272 pub source: String,
274 pub imported_name: ImportedName,
276 pub local_name: String,
278 pub is_type_only: bool,
280 pub span: Span,
282 pub source_span: Span,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
302pub enum ImportedName {
303 Named(String),
305 Default,
307 Namespace,
309 SideEffect,
311}
312
313#[cfg(target_pointer_width = "64")]
318const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
319#[cfg(target_pointer_width = "64")]
320const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
321#[cfg(target_pointer_width = "64")]
322const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
323#[cfg(target_pointer_width = "64")]
324const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
325#[cfg(target_pointer_width = "64")]
326const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
327#[cfg(target_pointer_width = "64")]
329const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 304);
330
331#[derive(Debug, Clone)]
333pub struct ReExportInfo {
334 pub source: String,
336 pub imported_name: String,
338 pub exported_name: String,
340 pub is_type_only: bool,
342}
343
344#[derive(Debug, Clone)]
346pub struct DynamicImportInfo {
347 pub source: String,
349 pub span: Span,
351 pub destructured_names: Vec<String>,
355 pub local_name: Option<String>,
358}
359
360#[derive(Debug, Clone)]
362pub struct RequireCallInfo {
363 pub source: String,
365 pub span: Span,
367 pub destructured_names: Vec<String>,
371 pub local_name: Option<String>,
374}
375
376pub struct ParseResult {
378 pub modules: Vec<ModuleInfo>,
380 pub cache_hits: usize,
382 pub cache_misses: usize,
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
393 fn line_offsets_empty_string() {
394 assert_eq!(compute_line_offsets(""), vec![0]);
395 }
396
397 #[test]
398 fn line_offsets_single_line_no_newline() {
399 assert_eq!(compute_line_offsets("hello"), vec![0]);
400 }
401
402 #[test]
403 fn line_offsets_single_line_with_newline() {
404 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
406 }
407
408 #[test]
409 fn line_offsets_multiple_lines() {
410 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
415 }
416
417 #[test]
418 fn line_offsets_trailing_newline() {
419 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
422 }
423
424 #[test]
425 fn line_offsets_consecutive_newlines() {
426 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
428 }
429
430 #[test]
431 fn line_offsets_multibyte_utf8() {
432 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
434 }
435
436 #[test]
439 fn line_col_offset_zero() {
440 let offsets = compute_line_offsets("abc\ndef\nghi");
441 let (line, col) = byte_offset_to_line_col(&offsets, 0);
442 assert_eq!((line, col), (1, 0)); }
444
445 #[test]
446 fn line_col_middle_of_first_line() {
447 let offsets = compute_line_offsets("abc\ndef\nghi");
448 let (line, col) = byte_offset_to_line_col(&offsets, 2);
449 assert_eq!((line, col), (1, 2)); }
451
452 #[test]
453 fn line_col_start_of_second_line() {
454 let offsets = compute_line_offsets("abc\ndef\nghi");
455 let (line, col) = byte_offset_to_line_col(&offsets, 4);
457 assert_eq!((line, col), (2, 0));
458 }
459
460 #[test]
461 fn line_col_middle_of_second_line() {
462 let offsets = compute_line_offsets("abc\ndef\nghi");
463 let (line, col) = byte_offset_to_line_col(&offsets, 5);
465 assert_eq!((line, col), (2, 1));
466 }
467
468 #[test]
469 fn line_col_start_of_third_line() {
470 let offsets = compute_line_offsets("abc\ndef\nghi");
471 let (line, col) = byte_offset_to_line_col(&offsets, 8);
473 assert_eq!((line, col), (3, 0));
474 }
475
476 #[test]
477 fn line_col_end_of_file() {
478 let offsets = compute_line_offsets("abc\ndef\nghi");
479 let (line, col) = byte_offset_to_line_col(&offsets, 10);
481 assert_eq!((line, col), (3, 2));
482 }
483
484 #[test]
485 fn line_col_single_line() {
486 let offsets = compute_line_offsets("hello");
487 let (line, col) = byte_offset_to_line_col(&offsets, 3);
488 assert_eq!((line, col), (1, 3));
489 }
490
491 #[test]
492 fn line_col_at_newline_byte() {
493 let offsets = compute_line_offsets("abc\ndef");
494 let (line, col) = byte_offset_to_line_col(&offsets, 3);
496 assert_eq!((line, col), (1, 3));
497 }
498
499 #[test]
502 fn export_name_matches_str_named() {
503 let name = ExportName::Named("foo".to_string());
504 assert!(name.matches_str("foo"));
505 assert!(!name.matches_str("bar"));
506 assert!(!name.matches_str("default"));
507 }
508
509 #[test]
510 fn export_name_matches_str_default() {
511 let name = ExportName::Default;
512 assert!(name.matches_str("default"));
513 assert!(!name.matches_str("foo"));
514 }
515
516 #[test]
517 fn export_name_display_named() {
518 let name = ExportName::Named("myExport".to_string());
519 assert_eq!(name.to_string(), "myExport");
520 }
521
522 #[test]
523 fn export_name_display_default() {
524 let name = ExportName::Default;
525 assert_eq!(name.to_string(), "default");
526 }
527
528 #[test]
531 fn export_name_equality_named() {
532 let a = ExportName::Named("foo".to_string());
533 let b = ExportName::Named("foo".to_string());
534 let c = ExportName::Named("bar".to_string());
535 assert_eq!(a, b);
536 assert_ne!(a, c);
537 }
538
539 #[test]
540 fn export_name_equality_default() {
541 let a = ExportName::Default;
542 let b = ExportName::Default;
543 assert_eq!(a, b);
544 }
545
546 #[test]
547 fn export_name_named_not_equal_to_default() {
548 let named = ExportName::Named("default".to_string());
549 let default = ExportName::Default;
550 assert_ne!(named, default);
551 }
552
553 #[test]
554 fn export_name_hash_consistency() {
555 use std::collections::hash_map::DefaultHasher;
556 use std::hash::{Hash, Hasher};
557
558 let mut h1 = DefaultHasher::new();
559 let mut h2 = DefaultHasher::new();
560 ExportName::Named("foo".to_string()).hash(&mut h1);
561 ExportName::Named("foo".to_string()).hash(&mut h2);
562 assert_eq!(h1.finish(), h2.finish());
563 }
564
565 #[test]
568 fn export_name_matches_str_empty_string() {
569 let name = ExportName::Named(String::new());
570 assert!(name.matches_str(""));
571 assert!(!name.matches_str("foo"));
572 }
573
574 #[test]
575 fn export_name_default_does_not_match_empty() {
576 let name = ExportName::Default;
577 assert!(!name.matches_str(""));
578 }
579
580 #[test]
583 fn imported_name_equality() {
584 assert_eq!(
585 ImportedName::Named("foo".to_string()),
586 ImportedName::Named("foo".to_string())
587 );
588 assert_ne!(
589 ImportedName::Named("foo".to_string()),
590 ImportedName::Named("bar".to_string())
591 );
592 assert_eq!(ImportedName::Default, ImportedName::Default);
593 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
594 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
595 assert_ne!(ImportedName::Default, ImportedName::Namespace);
596 assert_ne!(
597 ImportedName::Named("default".to_string()),
598 ImportedName::Default
599 );
600 }
601
602 #[test]
605 fn member_kind_equality() {
606 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
607 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
608 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
609 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
610 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
611 }
612
613 #[test]
616 fn member_kind_bincode_roundtrip() {
617 let kinds = [
618 MemberKind::EnumMember,
619 MemberKind::ClassMethod,
620 MemberKind::ClassProperty,
621 ];
622 let config = bincode::config::standard();
623 for kind in &kinds {
624 let bytes = bincode::encode_to_vec(kind, config).unwrap();
625 let (decoded, _): (MemberKind, _) = bincode::decode_from_slice(&bytes, config).unwrap();
626 assert_eq!(&decoded, kind);
627 }
628 }
629
630 #[test]
633 fn member_access_bincode_roundtrip() {
634 let access = MemberAccess {
635 object: "Status".to_string(),
636 member: "Active".to_string(),
637 };
638 let config = bincode::config::standard();
639 let bytes = bincode::encode_to_vec(&access, config).unwrap();
640 let (decoded, _): (MemberAccess, _) = bincode::decode_from_slice(&bytes, config).unwrap();
641 assert_eq!(decoded.object, "Status");
642 assert_eq!(decoded.member, "Active");
643 }
644
645 #[test]
648 fn line_offsets_crlf_only_counts_lf() {
649 let offsets = compute_line_offsets("ab\r\ncd");
653 assert_eq!(offsets, vec![0, 4]);
654 }
655
656 #[test]
659 fn line_col_empty_file_offset_zero() {
660 let offsets = compute_line_offsets("");
661 let (line, col) = byte_offset_to_line_col(&offsets, 0);
662 assert_eq!((line, col), (1, 0));
663 }
664
665 #[test]
668 fn function_complexity_bincode_roundtrip() {
669 let fc = FunctionComplexity {
670 name: "processData".to_string(),
671 line: 42,
672 col: 4,
673 cyclomatic: 15,
674 cognitive: 25,
675 line_count: 80,
676 };
677 let config = bincode::config::standard();
678 let bytes = bincode::encode_to_vec(&fc, config).unwrap();
679 let (decoded, _): (FunctionComplexity, _) =
680 bincode::decode_from_slice(&bytes, config).unwrap();
681 assert_eq!(decoded.name, "processData");
682 assert_eq!(decoded.line, 42);
683 assert_eq!(decoded.col, 4);
684 assert_eq!(decoded.cyclomatic, 15);
685 assert_eq!(decoded.cognitive, 25);
686 assert_eq!(decoded.line_count, 80);
687 }
688}