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}
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(
226 clippy::trivially_copy_pass_by_ref,
227 reason = "serde serialize_with requires &T"
228)]
229fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
230 use serde::ser::SerializeMap;
231 let mut map = serializer.serialize_map(Some(2))?;
232 map.serialize_entry("start", &span.start)?;
233 map.serialize_entry("end", &span.end)?;
234 map.end()
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
253pub enum ExportName {
254 Named(String),
256 Default,
258}
259
260impl ExportName {
261 #[must_use]
263 pub fn matches_str(&self, s: &str) -> bool {
264 match self {
265 Self::Named(n) => n == s,
266 Self::Default => s == "default",
267 }
268 }
269}
270
271impl std::fmt::Display for ExportName {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 match self {
274 Self::Named(n) => write!(f, "{n}"),
275 Self::Default => write!(f, "default"),
276 }
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct ImportInfo {
283 pub source: String,
285 pub imported_name: ImportedName,
287 pub local_name: String,
289 pub is_type_only: bool,
291 pub span: Span,
293 pub source_span: Span,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum ImportedName {
314 Named(String),
316 Default,
318 Namespace,
320 SideEffect,
322}
323
324#[cfg(target_pointer_width = "64")]
329const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
330#[cfg(target_pointer_width = "64")]
331const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
332#[cfg(target_pointer_width = "64")]
333const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
334#[cfg(target_pointer_width = "64")]
335const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
336#[cfg(target_pointer_width = "64")]
337const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
338#[cfg(target_pointer_width = "64")]
340const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 304);
341
342#[derive(Debug, Clone)]
344pub struct ReExportInfo {
345 pub source: String,
347 pub imported_name: String,
349 pub exported_name: String,
351 pub is_type_only: bool,
353}
354
355#[derive(Debug, Clone)]
357pub struct DynamicImportInfo {
358 pub source: String,
360 pub span: Span,
362 pub destructured_names: Vec<String>,
366 pub local_name: Option<String>,
369}
370
371#[derive(Debug, Clone)]
373pub struct RequireCallInfo {
374 pub source: String,
376 pub span: Span,
378 pub destructured_names: Vec<String>,
382 pub local_name: Option<String>,
385}
386
387pub struct ParseResult {
389 pub modules: Vec<ModuleInfo>,
391 pub cache_hits: usize,
393 pub cache_misses: usize,
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
404 fn line_offsets_empty_string() {
405 assert_eq!(compute_line_offsets(""), vec![0]);
406 }
407
408 #[test]
409 fn line_offsets_single_line_no_newline() {
410 assert_eq!(compute_line_offsets("hello"), vec![0]);
411 }
412
413 #[test]
414 fn line_offsets_single_line_with_newline() {
415 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
417 }
418
419 #[test]
420 fn line_offsets_multiple_lines() {
421 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
426 }
427
428 #[test]
429 fn line_offsets_trailing_newline() {
430 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
433 }
434
435 #[test]
436 fn line_offsets_consecutive_newlines() {
437 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
439 }
440
441 #[test]
442 fn line_offsets_multibyte_utf8() {
443 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
445 }
446
447 #[test]
450 fn line_col_offset_zero() {
451 let offsets = compute_line_offsets("abc\ndef\nghi");
452 let (line, col) = byte_offset_to_line_col(&offsets, 0);
453 assert_eq!((line, col), (1, 0)); }
455
456 #[test]
457 fn line_col_middle_of_first_line() {
458 let offsets = compute_line_offsets("abc\ndef\nghi");
459 let (line, col) = byte_offset_to_line_col(&offsets, 2);
460 assert_eq!((line, col), (1, 2)); }
462
463 #[test]
464 fn line_col_start_of_second_line() {
465 let offsets = compute_line_offsets("abc\ndef\nghi");
466 let (line, col) = byte_offset_to_line_col(&offsets, 4);
468 assert_eq!((line, col), (2, 0));
469 }
470
471 #[test]
472 fn line_col_middle_of_second_line() {
473 let offsets = compute_line_offsets("abc\ndef\nghi");
474 let (line, col) = byte_offset_to_line_col(&offsets, 5);
476 assert_eq!((line, col), (2, 1));
477 }
478
479 #[test]
480 fn line_col_start_of_third_line() {
481 let offsets = compute_line_offsets("abc\ndef\nghi");
482 let (line, col) = byte_offset_to_line_col(&offsets, 8);
484 assert_eq!((line, col), (3, 0));
485 }
486
487 #[test]
488 fn line_col_end_of_file() {
489 let offsets = compute_line_offsets("abc\ndef\nghi");
490 let (line, col) = byte_offset_to_line_col(&offsets, 10);
492 assert_eq!((line, col), (3, 2));
493 }
494
495 #[test]
496 fn line_col_single_line() {
497 let offsets = compute_line_offsets("hello");
498 let (line, col) = byte_offset_to_line_col(&offsets, 3);
499 assert_eq!((line, col), (1, 3));
500 }
501
502 #[test]
503 fn line_col_at_newline_byte() {
504 let offsets = compute_line_offsets("abc\ndef");
505 let (line, col) = byte_offset_to_line_col(&offsets, 3);
507 assert_eq!((line, col), (1, 3));
508 }
509
510 #[test]
513 fn export_name_matches_str_named() {
514 let name = ExportName::Named("foo".to_string());
515 assert!(name.matches_str("foo"));
516 assert!(!name.matches_str("bar"));
517 assert!(!name.matches_str("default"));
518 }
519
520 #[test]
521 fn export_name_matches_str_default() {
522 let name = ExportName::Default;
523 assert!(name.matches_str("default"));
524 assert!(!name.matches_str("foo"));
525 }
526
527 #[test]
528 fn export_name_display_named() {
529 let name = ExportName::Named("myExport".to_string());
530 assert_eq!(name.to_string(), "myExport");
531 }
532
533 #[test]
534 fn export_name_display_default() {
535 let name = ExportName::Default;
536 assert_eq!(name.to_string(), "default");
537 }
538
539 #[test]
542 fn export_name_equality_named() {
543 let a = ExportName::Named("foo".to_string());
544 let b = ExportName::Named("foo".to_string());
545 let c = ExportName::Named("bar".to_string());
546 assert_eq!(a, b);
547 assert_ne!(a, c);
548 }
549
550 #[test]
551 fn export_name_equality_default() {
552 let a = ExportName::Default;
553 let b = ExportName::Default;
554 assert_eq!(a, b);
555 }
556
557 #[test]
558 fn export_name_named_not_equal_to_default() {
559 let named = ExportName::Named("default".to_string());
560 let default = ExportName::Default;
561 assert_ne!(named, default);
562 }
563
564 #[test]
565 fn export_name_hash_consistency() {
566 use std::collections::hash_map::DefaultHasher;
567 use std::hash::{Hash, Hasher};
568
569 let mut h1 = DefaultHasher::new();
570 let mut h2 = DefaultHasher::new();
571 ExportName::Named("foo".to_string()).hash(&mut h1);
572 ExportName::Named("foo".to_string()).hash(&mut h2);
573 assert_eq!(h1.finish(), h2.finish());
574 }
575
576 #[test]
579 fn export_name_matches_str_empty_string() {
580 let name = ExportName::Named(String::new());
581 assert!(name.matches_str(""));
582 assert!(!name.matches_str("foo"));
583 }
584
585 #[test]
586 fn export_name_default_does_not_match_empty() {
587 let name = ExportName::Default;
588 assert!(!name.matches_str(""));
589 }
590
591 #[test]
594 fn imported_name_equality() {
595 assert_eq!(
596 ImportedName::Named("foo".to_string()),
597 ImportedName::Named("foo".to_string())
598 );
599 assert_ne!(
600 ImportedName::Named("foo".to_string()),
601 ImportedName::Named("bar".to_string())
602 );
603 assert_eq!(ImportedName::Default, ImportedName::Default);
604 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
605 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
606 assert_ne!(ImportedName::Default, ImportedName::Namespace);
607 assert_ne!(
608 ImportedName::Named("default".to_string()),
609 ImportedName::Default
610 );
611 }
612
613 #[test]
616 fn member_kind_equality() {
617 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
618 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
619 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
620 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
621 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
622 }
623
624 #[test]
627 fn member_kind_bincode_roundtrip() {
628 let kinds = [
629 MemberKind::EnumMember,
630 MemberKind::ClassMethod,
631 MemberKind::ClassProperty,
632 ];
633 let config = bincode::config::standard();
634 for kind in &kinds {
635 let bytes = bincode::encode_to_vec(kind, config).unwrap();
636 let (decoded, _): (MemberKind, _) = bincode::decode_from_slice(&bytes, config).unwrap();
637 assert_eq!(&decoded, kind);
638 }
639 }
640
641 #[test]
644 fn member_access_bincode_roundtrip() {
645 let access = MemberAccess {
646 object: "Status".to_string(),
647 member: "Active".to_string(),
648 };
649 let config = bincode::config::standard();
650 let bytes = bincode::encode_to_vec(&access, config).unwrap();
651 let (decoded, _): (MemberAccess, _) = bincode::decode_from_slice(&bytes, config).unwrap();
652 assert_eq!(decoded.object, "Status");
653 assert_eq!(decoded.member, "Active");
654 }
655
656 #[test]
659 fn line_offsets_crlf_only_counts_lf() {
660 let offsets = compute_line_offsets("ab\r\ncd");
664 assert_eq!(offsets, vec![0, 4]);
665 }
666
667 #[test]
670 fn line_col_empty_file_offset_zero() {
671 let offsets = compute_line_offsets("");
672 let (line, col) = byte_offset_to_line_col(&offsets, 0);
673 assert_eq!((line, col), (1, 0));
674 }
675
676 #[test]
679 fn function_complexity_bincode_roundtrip() {
680 let fc = FunctionComplexity {
681 name: "processData".to_string(),
682 line: 42,
683 col: 4,
684 cyclomatic: 15,
685 cognitive: 25,
686 line_count: 80,
687 };
688 let config = bincode::config::standard();
689 let bytes = bincode::encode_to_vec(&fc, config).unwrap();
690 let (decoded, _): (FunctionComplexity, _) =
691 bincode::decode_from_slice(&bytes, config).unwrap();
692 assert_eq!(decoded.name, "processData");
693 assert_eq!(decoded.line, 42);
694 assert_eq!(decoded.col, 4);
695 assert_eq!(decoded.cyclomatic, 15);
696 assert_eq!(decoded.cognitive, 25);
697 assert_eq!(decoded.line_count, 80);
698 }
699}