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 offsets.push((i + 1) as u32);
69 }
70 }
71 offsets
72}
73
74#[must_use]
90pub fn byte_offset_to_line_col(line_offsets: &[u32], byte_offset: u32) -> (u32, u32) {
91 let line_idx = match line_offsets.binary_search(&byte_offset) {
93 Ok(idx) => idx,
94 Err(idx) => idx.saturating_sub(1),
95 };
96 let line = line_idx as u32 + 1; let col = byte_offset - line_offsets[line_idx];
98 (line, col)
99}
100
101#[derive(Debug, Clone, serde::Serialize, bincode::Encode, bincode::Decode)]
103pub struct FunctionComplexity {
104 pub name: String,
106 pub line: u32,
108 pub col: u32,
110 pub cyclomatic: u16,
112 pub cognitive: u16,
114 pub line_count: u32,
116}
117
118#[derive(Debug, Clone)]
120pub struct DynamicImportPattern {
121 pub prefix: String,
123 pub suffix: Option<String>,
125 pub span: Span,
127}
128
129#[derive(Debug, Clone, serde::Serialize)]
131pub struct ExportInfo {
132 pub name: ExportName,
134 pub local_name: Option<String>,
136 pub is_type_only: bool,
138 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
142 pub is_public: bool,
143 #[serde(serialize_with = "serialize_span")]
145 pub span: Span,
146 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub members: Vec<MemberInfo>,
149}
150
151#[derive(Debug, Clone, serde::Serialize)]
153pub struct MemberInfo {
154 pub name: String,
156 pub kind: MemberKind,
158 #[serde(serialize_with = "serialize_span")]
160 pub span: Span,
161 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
165 pub has_decorator: bool,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, bincode::Encode, bincode::Decode)]
181#[serde(rename_all = "snake_case")]
182pub enum MemberKind {
183 EnumMember,
185 ClassMethod,
187 ClassProperty,
189}
190
191#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
206pub struct MemberAccess {
207 pub object: String,
209 pub member: String,
211}
212
213#[expect(clippy::trivially_copy_pass_by_ref)] fn serialize_span<S: serde::Serializer>(span: &Span, serializer: S) -> Result<S::Ok, S::Error> {
215 use serde::ser::SerializeMap;
216 let mut map = serializer.serialize_map(Some(2))?;
217 map.serialize_entry("start", &span.start)?;
218 map.serialize_entry("end", &span.end)?;
219 map.end()
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
238pub enum ExportName {
239 Named(String),
241 Default,
243}
244
245impl ExportName {
246 #[must_use]
248 pub fn matches_str(&self, s: &str) -> bool {
249 match self {
250 Self::Named(n) => n == s,
251 Self::Default => s == "default",
252 }
253 }
254}
255
256impl std::fmt::Display for ExportName {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 match self {
259 Self::Named(n) => write!(f, "{n}"),
260 Self::Default => write!(f, "default"),
261 }
262 }
263}
264
265#[derive(Debug, Clone)]
267pub struct ImportInfo {
268 pub source: String,
270 pub imported_name: ImportedName,
272 pub local_name: String,
274 pub is_type_only: bool,
276 pub span: Span,
278 pub source_span: Span,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum ImportedName {
299 Named(String),
301 Default,
303 Namespace,
305 SideEffect,
307}
308
309#[cfg(target_pointer_width = "64")]
314const _: () = assert!(std::mem::size_of::<ExportInfo>() == 88);
315#[cfg(target_pointer_width = "64")]
316const _: () = assert!(std::mem::size_of::<ImportInfo>() == 96);
317#[cfg(target_pointer_width = "64")]
318const _: () = assert!(std::mem::size_of::<ExportName>() == 24);
319#[cfg(target_pointer_width = "64")]
320const _: () = assert!(std::mem::size_of::<ImportedName>() == 24);
321#[cfg(target_pointer_width = "64")]
322const _: () = assert!(std::mem::size_of::<MemberAccess>() == 48);
323#[cfg(target_pointer_width = "64")]
325const _: () = assert!(std::mem::size_of::<ModuleInfo>() == 304);
326
327#[derive(Debug, Clone)]
329pub struct ReExportInfo {
330 pub source: String,
332 pub imported_name: String,
334 pub exported_name: String,
336 pub is_type_only: bool,
338}
339
340#[derive(Debug, Clone)]
342pub struct DynamicImportInfo {
343 pub source: String,
345 pub span: Span,
347 pub destructured_names: Vec<String>,
351 pub local_name: Option<String>,
354}
355
356#[derive(Debug, Clone)]
358pub struct RequireCallInfo {
359 pub source: String,
361 pub span: Span,
363 pub destructured_names: Vec<String>,
367 pub local_name: Option<String>,
370}
371
372pub struct ParseResult {
374 pub modules: Vec<ModuleInfo>,
376 pub cache_hits: usize,
378 pub cache_misses: usize,
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
389 fn line_offsets_empty_string() {
390 assert_eq!(compute_line_offsets(""), vec![0]);
391 }
392
393 #[test]
394 fn line_offsets_single_line_no_newline() {
395 assert_eq!(compute_line_offsets("hello"), vec![0]);
396 }
397
398 #[test]
399 fn line_offsets_single_line_with_newline() {
400 assert_eq!(compute_line_offsets("hello\n"), vec![0, 6]);
402 }
403
404 #[test]
405 fn line_offsets_multiple_lines() {
406 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
411 }
412
413 #[test]
414 fn line_offsets_trailing_newline() {
415 assert_eq!(compute_line_offsets("abc\ndef\n"), vec![0, 4, 8]);
418 }
419
420 #[test]
421 fn line_offsets_consecutive_newlines() {
422 assert_eq!(compute_line_offsets("\n\n\n"), vec![0, 1, 2, 3]);
424 }
425
426 #[test]
427 fn line_offsets_multibyte_utf8() {
428 assert_eq!(compute_line_offsets("á\n"), vec![0, 3]);
430 }
431
432 #[test]
435 fn line_col_offset_zero() {
436 let offsets = compute_line_offsets("abc\ndef\nghi");
437 let (line, col) = byte_offset_to_line_col(&offsets, 0);
438 assert_eq!((line, col), (1, 0)); }
440
441 #[test]
442 fn line_col_middle_of_first_line() {
443 let offsets = compute_line_offsets("abc\ndef\nghi");
444 let (line, col) = byte_offset_to_line_col(&offsets, 2);
445 assert_eq!((line, col), (1, 2)); }
447
448 #[test]
449 fn line_col_start_of_second_line() {
450 let offsets = compute_line_offsets("abc\ndef\nghi");
451 let (line, col) = byte_offset_to_line_col(&offsets, 4);
453 assert_eq!((line, col), (2, 0));
454 }
455
456 #[test]
457 fn line_col_middle_of_second_line() {
458 let offsets = compute_line_offsets("abc\ndef\nghi");
459 let (line, col) = byte_offset_to_line_col(&offsets, 5);
461 assert_eq!((line, col), (2, 1));
462 }
463
464 #[test]
465 fn line_col_start_of_third_line() {
466 let offsets = compute_line_offsets("abc\ndef\nghi");
467 let (line, col) = byte_offset_to_line_col(&offsets, 8);
469 assert_eq!((line, col), (3, 0));
470 }
471
472 #[test]
473 fn line_col_end_of_file() {
474 let offsets = compute_line_offsets("abc\ndef\nghi");
475 let (line, col) = byte_offset_to_line_col(&offsets, 10);
477 assert_eq!((line, col), (3, 2));
478 }
479
480 #[test]
481 fn line_col_single_line() {
482 let offsets = compute_line_offsets("hello");
483 let (line, col) = byte_offset_to_line_col(&offsets, 3);
484 assert_eq!((line, col), (1, 3));
485 }
486
487 #[test]
488 fn line_col_at_newline_byte() {
489 let offsets = compute_line_offsets("abc\ndef");
490 let (line, col) = byte_offset_to_line_col(&offsets, 3);
492 assert_eq!((line, col), (1, 3));
493 }
494
495 #[test]
498 fn export_name_matches_str_named() {
499 let name = ExportName::Named("foo".to_string());
500 assert!(name.matches_str("foo"));
501 assert!(!name.matches_str("bar"));
502 assert!(!name.matches_str("default"));
503 }
504
505 #[test]
506 fn export_name_matches_str_default() {
507 let name = ExportName::Default;
508 assert!(name.matches_str("default"));
509 assert!(!name.matches_str("foo"));
510 }
511
512 #[test]
513 fn export_name_display_named() {
514 let name = ExportName::Named("myExport".to_string());
515 assert_eq!(name.to_string(), "myExport");
516 }
517
518 #[test]
519 fn export_name_display_default() {
520 let name = ExportName::Default;
521 assert_eq!(name.to_string(), "default");
522 }
523
524 #[test]
527 fn export_name_equality_named() {
528 let a = ExportName::Named("foo".to_string());
529 let b = ExportName::Named("foo".to_string());
530 let c = ExportName::Named("bar".to_string());
531 assert_eq!(a, b);
532 assert_ne!(a, c);
533 }
534
535 #[test]
536 fn export_name_equality_default() {
537 let a = ExportName::Default;
538 let b = ExportName::Default;
539 assert_eq!(a, b);
540 }
541
542 #[test]
543 fn export_name_named_not_equal_to_default() {
544 let named = ExportName::Named("default".to_string());
545 let default = ExportName::Default;
546 assert_ne!(named, default);
547 }
548
549 #[test]
550 fn export_name_hash_consistency() {
551 use std::collections::hash_map::DefaultHasher;
552 use std::hash::{Hash, Hasher};
553
554 let mut h1 = DefaultHasher::new();
555 let mut h2 = DefaultHasher::new();
556 ExportName::Named("foo".to_string()).hash(&mut h1);
557 ExportName::Named("foo".to_string()).hash(&mut h2);
558 assert_eq!(h1.finish(), h2.finish());
559 }
560
561 #[test]
564 fn export_name_matches_str_empty_string() {
565 let name = ExportName::Named(String::new());
566 assert!(name.matches_str(""));
567 assert!(!name.matches_str("foo"));
568 }
569
570 #[test]
571 fn export_name_default_does_not_match_empty() {
572 let name = ExportName::Default;
573 assert!(!name.matches_str(""));
574 }
575
576 #[test]
579 fn imported_name_equality() {
580 assert_eq!(
581 ImportedName::Named("foo".to_string()),
582 ImportedName::Named("foo".to_string())
583 );
584 assert_ne!(
585 ImportedName::Named("foo".to_string()),
586 ImportedName::Named("bar".to_string())
587 );
588 assert_eq!(ImportedName::Default, ImportedName::Default);
589 assert_eq!(ImportedName::Namespace, ImportedName::Namespace);
590 assert_eq!(ImportedName::SideEffect, ImportedName::SideEffect);
591 assert_ne!(ImportedName::Default, ImportedName::Namespace);
592 assert_ne!(
593 ImportedName::Named("default".to_string()),
594 ImportedName::Default
595 );
596 }
597
598 #[test]
601 fn member_kind_equality() {
602 assert_eq!(MemberKind::EnumMember, MemberKind::EnumMember);
603 assert_eq!(MemberKind::ClassMethod, MemberKind::ClassMethod);
604 assert_eq!(MemberKind::ClassProperty, MemberKind::ClassProperty);
605 assert_ne!(MemberKind::EnumMember, MemberKind::ClassMethod);
606 assert_ne!(MemberKind::ClassMethod, MemberKind::ClassProperty);
607 }
608
609 #[test]
612 fn member_kind_bincode_roundtrip() {
613 let kinds = [
614 MemberKind::EnumMember,
615 MemberKind::ClassMethod,
616 MemberKind::ClassProperty,
617 ];
618 let config = bincode::config::standard();
619 for kind in &kinds {
620 let bytes = bincode::encode_to_vec(kind, config).unwrap();
621 let (decoded, _): (MemberKind, _) = bincode::decode_from_slice(&bytes, config).unwrap();
622 assert_eq!(&decoded, kind);
623 }
624 }
625
626 #[test]
629 fn member_access_bincode_roundtrip() {
630 let access = MemberAccess {
631 object: "Status".to_string(),
632 member: "Active".to_string(),
633 };
634 let config = bincode::config::standard();
635 let bytes = bincode::encode_to_vec(&access, config).unwrap();
636 let (decoded, _): (MemberAccess, _) = bincode::decode_from_slice(&bytes, config).unwrap();
637 assert_eq!(decoded.object, "Status");
638 assert_eq!(decoded.member, "Active");
639 }
640
641 #[test]
644 fn line_offsets_crlf_only_counts_lf() {
645 let offsets = compute_line_offsets("ab\r\ncd");
649 assert_eq!(offsets, vec![0, 4]);
650 }
651
652 #[test]
655 fn line_col_empty_file_offset_zero() {
656 let offsets = compute_line_offsets("");
657 let (line, col) = byte_offset_to_line_col(&offsets, 0);
658 assert_eq!((line, col), (1, 0));
659 }
660
661 #[test]
664 fn function_complexity_bincode_roundtrip() {
665 let fc = FunctionComplexity {
666 name: "processData".to_string(),
667 line: 42,
668 col: 4,
669 cyclomatic: 15,
670 cognitive: 25,
671 line_count: 80,
672 };
673 let config = bincode::config::standard();
674 let bytes = bincode::encode_to_vec(&fc, config).unwrap();
675 let (decoded, _): (FunctionComplexity, _) =
676 bincode::decode_from_slice(&bytes, config).unwrap();
677 assert_eq!(decoded.name, "processData");
678 assert_eq!(decoded.line, 42);
679 assert_eq!(decoded.col, 4);
680 assert_eq!(decoded.cyclomatic, 15);
681 assert_eq!(decoded.cognitive, 25);
682 assert_eq!(decoded.line_count, 80);
683 }
684}