1use std::sync::{Arc, OnceLock};
8
9use crate::js_identifiers::is_valid_javascript_identifier;
10use crate::{OriginalLocation, SourceMap};
11
12#[derive(Debug, Clone, Copy)]
16struct LineRange {
17 start: usize,
18 end: usize,
19}
20
21#[derive(Debug, Clone)]
33pub struct SourceView {
34 source: Arc<str>,
35 line_cache: OnceLock<Vec<LineRange>>,
36}
37
38impl SourceView {
39 pub fn new(source: Arc<str>) -> Self {
41 Self {
42 source,
43 line_cache: OnceLock::new(),
44 }
45 }
46
47 pub fn from_string(source: String) -> Self {
49 Self::new(Arc::from(source))
50 }
51
52 #[inline]
54 pub fn source(&self) -> &str {
55 &self.source
56 }
57
58 pub fn line_count(&self) -> usize {
63 self.lines().len()
64 }
65
66 pub fn get_line(&self, idx: u32) -> Option<&str> {
71 let lines = self.lines();
72 let range = lines.get(idx as usize)?;
73 Some(&self.source[range.start..range.end])
74 }
75
76 pub fn get_line_slice(&self, line: u32, col: u32, span: u32) -> Option<&str> {
85 let line_str = self.get_line(line)?;
86 let start_byte = utf16_col_to_byte_offset(line_str, col)?;
87 let end_byte = utf16_offset_from(line_str, start_byte, span)?;
88 Some(&line_str[start_byte..end_byte])
89 }
90
91 pub fn get_original_function_name<'a>(
106 &self,
107 token: &OriginalLocation,
108 minified_name: &str,
109 sm: &'a SourceMap,
110 ) -> Option<&'a str> {
111 let source_name = sm.get_source(token.source)?;
113 let gen_loc = sm.generated_position_for(source_name, token.line, token.column)?;
114
115 let line_str = self.get_line(gen_loc.line)?;
116 let col_byte = utf16_col_to_byte_offset(line_str, gen_loc.column)?;
117
118 let prefix = &line_str[..col_byte];
120
121 let candidate = extract_function_name_candidate(prefix)?;
123
124 if !is_valid_javascript_identifier(candidate) {
125 return None;
126 }
127
128 if candidate != minified_name {
131 return None;
132 }
133
134 let candidate_start_byte = prefix.len() - candidate.len();
136 let candidate_col = byte_offset_to_utf16_col(line_str, candidate_start_byte);
137
138 let original = sm.original_position_for(gen_loc.line, candidate_col)?;
140 let name_idx = original.name?;
141 sm.get_name(name_idx)
142 }
143
144 fn lines(&self) -> &[LineRange] {
146 self.line_cache
147 .get_or_init(|| compute_line_ranges(&self.source))
148 }
149}
150
151fn compute_line_ranges(source: &str) -> Vec<LineRange> {
156 let bytes = source.as_bytes();
157 let len = bytes.len();
158
159 if len == 0 {
160 return vec![];
161 }
162
163 let mut ranges = Vec::new();
164 let mut start = 0;
165 let mut i = 0;
166
167 while i < len {
168 match bytes[i] {
169 b'\n' => {
170 ranges.push(LineRange { start, end: i });
171 start = i + 1;
172 i += 1;
173 }
174 b'\r' => {
175 ranges.push(LineRange { start, end: i });
176 if i + 1 < len && bytes[i + 1] == b'\n' {
178 i += 2;
179 } else {
180 i += 1;
181 }
182 start = i;
183 }
184 _ => {
185 i += 1;
186 }
187 }
188 }
189
190 if start < len {
192 ranges.push(LineRange { start, end: len });
193 }
194
195 ranges
196}
197
198fn utf16_col_to_byte_offset(s: &str, col: u32) -> Option<usize> {
202 if col == 0 {
203 return Some(0);
204 }
205
206 let mut utf16_offset = 0u32;
207 for (byte_idx, ch) in s.char_indices() {
208 if utf16_offset == col {
209 return Some(byte_idx);
210 }
211 utf16_offset += ch.len_utf16() as u32;
212 if utf16_offset > col {
213 return None;
215 }
216 }
217
218 if utf16_offset == col {
220 return Some(s.len());
221 }
222
223 None
224}
225
226fn utf16_offset_from(s: &str, start_byte: usize, span: u32) -> Option<usize> {
230 if span == 0 {
231 return Some(start_byte);
232 }
233
234 let tail = s.get(start_byte..)?;
235 let mut utf16_offset = 0u32;
236 for (byte_idx, ch) in tail.char_indices() {
237 if utf16_offset == span {
238 return Some(start_byte + byte_idx);
239 }
240 utf16_offset += ch.len_utf16() as u32;
241 if utf16_offset > span {
242 return None;
243 }
244 }
245
246 if utf16_offset == span {
247 return Some(start_byte + tail.len());
248 }
249
250 None
251}
252
253fn byte_offset_to_utf16_col(s: &str, byte_offset: usize) -> u32 {
255 let prefix = &s[..byte_offset];
256 prefix.chars().map(|c| c.len_utf16() as u32).sum()
257}
258
259fn extract_function_name_candidate(prefix: &str) -> Option<&str> {
268 let trimmed = prefix.trim_end();
269 if trimmed.is_empty() {
270 return None;
271 }
272
273 let last_char = trimmed.chars().next_back()?;
274
275 match last_char {
276 '(' | ',' => {
278 let before_paren = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
279 extract_trailing_identifier(before_paren)
280 }
281 ':' => {
283 let before_colon = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
284 extract_trailing_identifier(before_colon)
285 }
286 '=' => {
288 let before_eq_str = &trimmed[..trimmed.len() - 1];
290 if let Some(prev) = before_eq_str.chars().next_back()
291 && matches!(
292 prev,
293 '=' | '!' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '|' | '&' | '^' | '?'
294 )
295 {
296 return None;
297 }
298 let before_eq = before_eq_str.trim_end();
299 extract_trailing_identifier(before_eq)
300 }
301 _ if last_char.is_ascii_alphanumeric()
303 || last_char == '_'
304 || last_char == '$'
305 || (!last_char.is_ascii() && last_char.is_alphanumeric()) =>
306 {
307 let ident = extract_trailing_identifier(trimmed)?;
308 let before = trimmed[..trimmed.len() - ident.len()].trim_end();
309 if before.ends_with('.') {
310 return Some(ident);
311 }
312 if before.ends_with("var ")
314 || before.ends_with("let ")
315 || before.ends_with("const ")
316 || before.ends_with("function ")
317 {
318 return Some(ident);
319 }
320 Some(ident)
321 }
322 _ => None,
323 }
324}
325
326fn extract_trailing_identifier(s: &str) -> Option<&str> {
330 if s.is_empty() {
331 return None;
332 }
333
334 let end = s.len();
335 let mut chars = s.char_indices().rev().peekable();
336
337 let mut start = end;
339 while let Some((idx, ch)) = chars.peek() {
340 if ch.is_ascii_alphanumeric()
341 || *ch == '_'
342 || *ch == '$'
343 || *ch == '\u{200c}'
344 || *ch == '\u{200d}'
345 || (!ch.is_ascii() && ch.is_alphanumeric())
346 {
347 start = *idx;
348 chars.next();
349 } else {
350 break;
351 }
352 }
353
354 if start == end {
355 return None;
356 }
357
358 let ident = &s[start..end];
359
360 let first = ident.chars().next()?;
362 if first.is_ascii_digit() {
363 return None;
364 }
365
366 if is_valid_javascript_identifier(ident) {
367 Some(ident)
368 } else {
369 None
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use std::sync::Arc;
376
377 use crate::SourceMap;
378
379 use super::*;
380
381 #[test]
384 fn test_empty_source() {
385 let view = SourceView::from_string(String::new());
386 assert_eq!(view.line_count(), 0);
387 assert_eq!(view.get_line(0), None);
388 }
389
390 #[test]
391 fn test_single_line_no_newline() {
392 let view = SourceView::from_string("hello world".into());
393 assert_eq!(view.line_count(), 1);
394 assert_eq!(view.get_line(0), Some("hello world"));
395 assert_eq!(view.get_line(1), None);
396 }
397
398 #[test]
399 fn test_single_line_with_trailing_lf() {
400 let view = SourceView::from_string("hello\n".into());
401 assert_eq!(view.line_count(), 1);
402 assert_eq!(view.get_line(0), Some("hello"));
403 }
404
405 #[test]
406 fn test_multiple_lines_lf() {
407 let view = SourceView::from_string("line1\nline2\nline3".into());
408 assert_eq!(view.line_count(), 3);
409 assert_eq!(view.get_line(0), Some("line1"));
410 assert_eq!(view.get_line(1), Some("line2"));
411 assert_eq!(view.get_line(2), Some("line3"));
412 }
413
414 #[test]
415 fn test_multiple_lines_cr() {
416 let view = SourceView::from_string("line1\rline2\rline3".into());
417 assert_eq!(view.line_count(), 3);
418 assert_eq!(view.get_line(0), Some("line1"));
419 assert_eq!(view.get_line(1), Some("line2"));
420 assert_eq!(view.get_line(2), Some("line3"));
421 }
422
423 #[test]
424 fn test_multiple_lines_crlf() {
425 let view = SourceView::from_string("line1\r\nline2\r\nline3".into());
426 assert_eq!(view.line_count(), 3);
427 assert_eq!(view.get_line(0), Some("line1"));
428 assert_eq!(view.get_line(1), Some("line2"));
429 assert_eq!(view.get_line(2), Some("line3"));
430 }
431
432 #[test]
433 fn test_mixed_line_endings() {
434 let view = SourceView::from_string("a\nb\rc\r\nd".into());
435 assert_eq!(view.line_count(), 4);
436 assert_eq!(view.get_line(0), Some("a"));
437 assert_eq!(view.get_line(1), Some("b"));
438 assert_eq!(view.get_line(2), Some("c"));
439 assert_eq!(view.get_line(3), Some("d"));
440 }
441
442 #[test]
443 fn test_empty_lines() {
444 let view = SourceView::from_string("\n\n\n".into());
445 assert_eq!(view.line_count(), 3);
446 assert_eq!(view.get_line(0), Some(""));
447 assert_eq!(view.get_line(1), Some(""));
448 assert_eq!(view.get_line(2), Some(""));
449 }
450
451 #[test]
452 fn test_crlf_trailing() {
453 let view = SourceView::from_string("a\r\n".into());
454 assert_eq!(view.line_count(), 1);
455 assert_eq!(view.get_line(0), Some("a"));
456 }
457
458 #[test]
459 fn test_cr_trailing() {
460 let view = SourceView::from_string("a\r".into());
461 assert_eq!(view.line_count(), 1);
462 assert_eq!(view.get_line(0), Some("a"));
463 }
464
465 #[test]
468 fn test_get_line_slice_ascii() {
469 let view = SourceView::from_string("abcdefgh".into());
470 assert_eq!(view.get_line_slice(0, 2, 3), Some("cde"));
471 assert_eq!(view.get_line_slice(0, 0, 8), Some("abcdefgh"));
472 assert_eq!(view.get_line_slice(0, 0, 0), Some(""));
473 }
474
475 #[test]
476 fn test_get_line_slice_multibyte() {
477 let view = SourceView::from_string("\u{00e9}\u{00e8}\u{00ea}abc".into());
479 assert_eq!(
481 view.get_line_slice(0, 0, 3),
482 Some("\u{00e9}\u{00e8}\u{00ea}")
483 );
484 assert_eq!(view.get_line_slice(0, 3, 3), Some("abc"));
485 }
486
487 #[test]
488 fn test_get_line_slice_emoji_surrogate_pair() {
489 let view = SourceView::from_string("a\u{1F600}b".into());
491 assert_eq!(view.get_line_slice(0, 0, 1), Some("a"));
493 assert_eq!(view.get_line_slice(0, 1, 2), Some("\u{1F600}"));
494 assert_eq!(view.get_line_slice(0, 3, 1), Some("b"));
495 assert_eq!(view.get_line_slice(0, 0, 4), Some("a\u{1F600}b"));
496 }
497
498 #[test]
499 fn test_get_line_slice_surrogate_pair_middle() {
500 let view = SourceView::from_string("\u{1F600}".into());
502 assert_eq!(view.get_line_slice(0, 1, 1), None); }
504
505 #[test]
506 fn test_get_line_slice_out_of_bounds() {
507 let view = SourceView::from_string("abc".into());
508 assert_eq!(view.get_line_slice(0, 0, 10), None); assert_eq!(view.get_line_slice(0, 5, 1), None); assert_eq!(view.get_line_slice(1, 0, 1), None); }
512
513 #[test]
514 fn test_get_line_slice_cjk() {
515 let view = SourceView::from_string("x\u{4e16}\u{754c}y".into());
517 assert_eq!(view.get_line_slice(0, 1, 2), Some("\u{4e16}\u{754c}"));
518 }
519
520 #[test]
521 fn test_get_line_slice_multiline() {
522 let view = SourceView::from_string("abc\ndef\nghi".into());
523 assert_eq!(view.get_line_slice(0, 1, 2), Some("bc"));
524 assert_eq!(view.get_line_slice(1, 0, 3), Some("def"));
525 assert_eq!(view.get_line_slice(2, 2, 1), Some("i"));
526 }
527
528 #[test]
531 fn test_utf16_col_to_byte_offset_ascii() {
532 assert_eq!(utf16_col_to_byte_offset("abcd", 0), Some(0));
533 assert_eq!(utf16_col_to_byte_offset("abcd", 2), Some(2));
534 assert_eq!(utf16_col_to_byte_offset("abcd", 4), Some(4));
535 }
536
537 #[test]
538 fn test_utf16_col_to_byte_offset_multibyte() {
539 let s = "\u{00e9}a";
541 assert_eq!(utf16_col_to_byte_offset(s, 0), Some(0));
542 assert_eq!(utf16_col_to_byte_offset(s, 1), Some(2)); assert_eq!(utf16_col_to_byte_offset(s, 2), Some(3)); }
545
546 #[test]
547 fn test_utf16_col_to_byte_offset_surrogate_pair() {
548 let s = "\u{1F600}a";
550 assert_eq!(utf16_col_to_byte_offset(s, 0), Some(0));
551 assert_eq!(utf16_col_to_byte_offset(s, 1), None); assert_eq!(utf16_col_to_byte_offset(s, 2), Some(4)); assert_eq!(utf16_col_to_byte_offset(s, 3), Some(5)); }
555
556 #[test]
557 fn test_byte_offset_to_utf16_col() {
558 assert_eq!(byte_offset_to_utf16_col("abcd", 0), 0);
559 assert_eq!(byte_offset_to_utf16_col("abcd", 2), 2);
560 let s = "a\u{1F600}b";
562 assert_eq!(byte_offset_to_utf16_col(s, 0), 0);
563 assert_eq!(byte_offset_to_utf16_col(s, 1), 1); assert_eq!(byte_offset_to_utf16_col(s, 5), 3); assert_eq!(byte_offset_to_utf16_col(s, 6), 4); }
567
568 #[test]
571 fn test_extract_function_call() {
572 assert_eq!(extract_function_name_candidate("foo("), Some("foo"));
573 assert_eq!(extract_function_name_candidate(" bar("), Some("bar"));
574 assert_eq!(
575 extract_function_name_candidate("obj.method("),
576 Some("method")
577 );
578 }
579
580 #[test]
581 fn test_extract_assignment() {
582 assert_eq!(extract_function_name_candidate("x ="), Some("x"));
583 assert_eq!(extract_function_name_candidate("myVar ="), Some("myVar"));
584 assert_eq!(extract_function_name_candidate("x = "), Some("x"));
585 }
586
587 #[test]
588 fn test_extract_colon() {
589 assert_eq!(extract_function_name_candidate("key:"), Some("key"));
590 assert_eq!(extract_function_name_candidate(" prop:"), Some("prop"));
591 }
592
593 #[test]
594 fn test_extract_comparison_operators() {
595 assert_eq!(extract_function_name_candidate("x =="), None);
597 assert_eq!(extract_function_name_candidate("x !="), None);
598 assert_eq!(extract_function_name_candidate("x >="), None);
599 assert_eq!(extract_function_name_candidate("x <="), None);
600 }
601
602 #[test]
603 fn test_extract_member_access() {
604 assert_eq!(extract_function_name_candidate("obj.prop"), Some("prop"));
605 assert_eq!(
606 extract_function_name_candidate("window.addEventListener"),
607 Some("addEventListener")
608 );
609 }
610
611 #[test]
612 fn test_extract_variable_declaration() {
613 assert_eq!(extract_function_name_candidate("var x"), Some("x"));
614 assert_eq!(extract_function_name_candidate("let myVar"), Some("myVar"));
615 assert_eq!(
616 extract_function_name_candidate("const CONSTANT"),
617 Some("CONSTANT")
618 );
619 }
620
621 #[test]
622 fn test_extract_none() {
623 assert_eq!(extract_function_name_candidate(""), None);
624 assert_eq!(extract_function_name_candidate(" "), None);
625 assert_eq!(extract_function_name_candidate("123"), None);
626 }
627
628 #[test]
629 fn test_extract_comma_separated() {
630 assert_eq!(extract_function_name_candidate("foo(a,"), Some("a"));
631 }
632
633 #[test]
636 fn test_arc_construction() {
637 let source: Arc<str> = Arc::from("test source");
638 let view = SourceView::new(source.clone());
639 assert_eq!(view.source(), "test source");
640 }
641
642 #[test]
643 fn test_send_sync() {
644 fn assert_send_sync<T: Send + Sync>() {}
645 assert_send_sync::<SourceView>();
646 }
647
648 #[test]
649 fn test_clone() {
650 let view = SourceView::from_string("line1\nline2".into());
651 assert_eq!(view.line_count(), 2);
653 let view2 = view.clone();
654 assert_eq!(view2.line_count(), 2);
655 assert_eq!(view2.get_line(0), Some("line1"));
656 }
657
658 #[test]
661 fn test_get_original_function_name() {
662 let json = r#"{
665 "version": 3,
666 "sources": ["input.js"],
667 "names": ["originalFunc", "originalArg"],
668 "mappings": "AAAA,CAAC"
669 }"#;
670
671 let sm = SourceMap::from_json(json).unwrap();
672
673 let view = SourceView::from_string("a(b)".into());
680 let token = OriginalLocation {
681 source: 0,
682 line: 0,
683 column: 0,
684 name: Some(0),
685 };
686 let result = view.get_original_function_name(&token, "nonexistent", &sm);
688 assert_eq!(result, None);
689 }
690
691 #[test]
692 fn test_get_original_function_name_with_match() {
693 let json = r#"{
705 "version": 3,
706 "sources": ["input.js"],
707 "names": ["originalFunc", "originalArg"],
708 "mappings": "AAAAA,EAAKC"
709 }"#;
710
711 let sm = SourceMap::from_json(json).unwrap();
712
713 let loc0 = sm.original_position_for(0, 0).unwrap();
715 assert_eq!(loc0.source, 0);
716 assert_eq!(loc0.line, 0);
717 assert_eq!(loc0.column, 0);
718 assert_eq!(loc0.name, Some(0));
719
720 let loc2 = sm.original_position_for(0, 2).unwrap();
721 assert_eq!(loc2.source, 0);
722 assert_eq!(loc2.line, 0);
723 assert_eq!(loc2.column, 5);
724 assert_eq!(loc2.name, Some(1));
725
726 let view = SourceView::from_string("a(b)".into());
728
729 let token = OriginalLocation {
732 source: 0,
733 line: 0,
734 column: 5,
735 name: Some(1),
736 };
737
738 let result = view.get_original_function_name(&token, "a", &sm);
740 assert_eq!(result, Some("originalFunc"));
741 }
742
743 #[test]
744 fn test_line_cache_consistency() {
745 let view = SourceView::from_string("a\nb\nc".into());
746 assert_eq!(view.get_line(2), Some("c"));
748 assert_eq!(view.get_line(0), Some("a"));
749 assert_eq!(view.get_line(1), Some("b"));
750 assert_eq!(view.line_count(), 3);
751 }
752
753 #[test]
754 fn test_only_newlines() {
755 let view = SourceView::from_string("\n".into());
756 assert_eq!(view.line_count(), 1);
757 assert_eq!(view.get_line(0), Some(""));
758 }
759
760 #[test]
761 fn test_consecutive_crlf() {
762 let view = SourceView::from_string("\r\n\r\n".into());
763 assert_eq!(view.line_count(), 2);
764 assert_eq!(view.get_line(0), Some(""));
765 assert_eq!(view.get_line(1), Some(""));
766 }
767
768 #[test]
769 fn test_unicode_line_content() {
770 let view = SourceView::from_string("Hello \u{4e16}\u{754c}\n\u{1F600} smile".into());
771 assert_eq!(view.line_count(), 2);
772 assert_eq!(view.get_line(0), Some("Hello \u{4e16}\u{754c}"));
773 assert_eq!(view.get_line(1), Some("\u{1F600} smile"));
774 }
775
776 #[test]
777 fn test_get_line_slice_at_line_end() {
778 let view = SourceView::from_string("abc".into());
779 assert_eq!(view.get_line_slice(0, 3, 0), Some(""));
781 }
782
783 #[test]
784 fn test_get_line_slice_full_line() {
785 let view = SourceView::from_string("abc\ndef".into());
786 assert_eq!(view.get_line_slice(0, 0, 3), Some("abc"));
787 assert_eq!(view.get_line_slice(1, 0, 3), Some("def"));
788 }
789}