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 { source, line_cache: OnceLock::new() }
42 }
43
44 pub fn from_string(source: String) -> Self {
46 Self::new(Arc::from(source))
47 }
48
49 #[inline]
51 pub fn source(&self) -> &str {
52 &self.source
53 }
54
55 pub fn line_count(&self) -> usize {
60 self.lines().len()
61 }
62
63 pub fn get_line(&self, idx: u32) -> Option<&str> {
68 let lines = self.lines();
69 let range = lines.get(idx as usize)?;
70 Some(&self.source[range.start..range.end])
71 }
72
73 pub fn get_line_slice(&self, line: u32, col: u32, span: u32) -> Option<&str> {
82 let line_str = self.get_line(line)?;
83 let start_byte = utf16_col_to_byte_offset(line_str, col)?;
84 let end_byte = utf16_offset_from(line_str, start_byte, span)?;
85 Some(&line_str[start_byte..end_byte])
86 }
87
88 pub fn get_original_function_name<'a>(
103 &self,
104 token: &OriginalLocation,
105 minified_name: &str,
106 sm: &'a SourceMap,
107 ) -> Option<&'a str> {
108 let source_name = sm.get_source(token.source)?;
110 let gen_loc = sm.generated_position_for(source_name, token.line, token.column)?;
111
112 let line_str = self.get_line(gen_loc.line)?;
113 let col_byte = utf16_col_to_byte_offset(line_str, gen_loc.column)?;
114
115 let prefix = &line_str[..col_byte];
117
118 let candidate = extract_function_name_candidate(prefix)?;
120
121 if !is_valid_javascript_identifier(candidate) {
122 return None;
123 }
124
125 if candidate != minified_name {
128 return None;
129 }
130
131 let candidate_start_byte = prefix.len() - candidate.len();
133 let candidate_col = byte_offset_to_utf16_col(line_str, candidate_start_byte);
134
135 let original = sm.original_position_for(gen_loc.line, candidate_col)?;
137 let name_idx = original.name?;
138 sm.get_name(name_idx)
139 }
140
141 fn lines(&self) -> &[LineRange] {
143 self.line_cache.get_or_init(|| compute_line_ranges(&self.source))
144 }
145}
146
147fn compute_line_ranges(source: &str) -> Vec<LineRange> {
152 let bytes = source.as_bytes();
153 let len = bytes.len();
154
155 if len == 0 {
156 return vec![];
157 }
158
159 let mut ranges = Vec::new();
160 let mut start = 0;
161 let mut i = 0;
162
163 while i < len {
164 match bytes[i] {
165 b'\n' => {
166 ranges.push(LineRange { start, end: i });
167 start = i + 1;
168 i += 1;
169 }
170 b'\r' => {
171 ranges.push(LineRange { start, end: i });
172 if i + 1 < len && bytes[i + 1] == b'\n' {
174 i += 2;
175 } else {
176 i += 1;
177 }
178 start = i;
179 }
180 _ => {
181 i += 1;
182 }
183 }
184 }
185
186 if start < len {
188 ranges.push(LineRange { start, end: len });
189 }
190
191 ranges
192}
193
194fn utf16_col_to_byte_offset(s: &str, col: u32) -> Option<usize> {
198 if col == 0 {
199 return Some(0);
200 }
201
202 let mut utf16_offset = 0u32;
203 for (byte_idx, ch) in s.char_indices() {
204 if utf16_offset == col {
205 return Some(byte_idx);
206 }
207 utf16_offset += ch.len_utf16() as u32;
208 if utf16_offset > col {
209 return None;
211 }
212 }
213
214 if utf16_offset == col {
216 return Some(s.len());
217 }
218
219 None
220}
221
222fn utf16_offset_from(s: &str, start_byte: usize, span: u32) -> Option<usize> {
226 if span == 0 {
227 return Some(start_byte);
228 }
229
230 let tail = s.get(start_byte..)?;
231 let mut utf16_offset = 0u32;
232 for (byte_idx, ch) in tail.char_indices() {
233 if utf16_offset == span {
234 return Some(start_byte + byte_idx);
235 }
236 utf16_offset += ch.len_utf16() as u32;
237 if utf16_offset > span {
238 return None;
239 }
240 }
241
242 if utf16_offset == span {
243 return Some(start_byte + tail.len());
244 }
245
246 None
247}
248
249fn byte_offset_to_utf16_col(s: &str, byte_offset: usize) -> u32 {
251 let prefix = &s[..byte_offset];
252 prefix.chars().map(|c| c.len_utf16() as u32).sum()
253}
254
255fn extract_function_name_candidate(prefix: &str) -> Option<&str> {
264 let trimmed = prefix.trim_end();
265 if trimmed.is_empty() {
266 return None;
267 }
268
269 let last_char = trimmed.chars().next_back()?;
270
271 match last_char {
272 '(' | ',' => {
274 let before_paren = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
275 extract_trailing_identifier(before_paren)
276 }
277 ':' => {
279 let before_colon = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
280 extract_trailing_identifier(before_colon)
281 }
282 '=' => {
284 let before_eq_str = &trimmed[..trimmed.len() - 1];
286 if let Some(prev) = before_eq_str.chars().next_back()
287 && matches!(
288 prev,
289 '=' | '!' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '|' | '&' | '^' | '?'
290 )
291 {
292 return None;
293 }
294 let before_eq = before_eq_str.trim_end();
295 extract_trailing_identifier(before_eq)
296 }
297 _ if last_char.is_ascii_alphanumeric()
299 || last_char == '_'
300 || last_char == '$'
301 || (!last_char.is_ascii() && last_char.is_alphanumeric()) =>
302 {
303 let ident = extract_trailing_identifier(trimmed)?;
304 let before = trimmed[..trimmed.len() - ident.len()].trim_end();
305 if before.ends_with('.') {
306 return Some(ident);
307 }
308 if before.ends_with("var ")
310 || before.ends_with("let ")
311 || before.ends_with("const ")
312 || before.ends_with("function ")
313 {
314 return Some(ident);
315 }
316 Some(ident)
317 }
318 _ => None,
319 }
320}
321
322fn extract_trailing_identifier(s: &str) -> Option<&str> {
326 if s.is_empty() {
327 return None;
328 }
329
330 let end = s.len();
331 let mut chars = s.char_indices().rev().peekable();
332
333 let mut start = end;
335 while let Some((idx, ch)) = chars.peek() {
336 if ch.is_ascii_alphanumeric()
337 || *ch == '_'
338 || *ch == '$'
339 || *ch == '\u{200c}'
340 || *ch == '\u{200d}'
341 || (!ch.is_ascii() && ch.is_alphanumeric())
342 {
343 start = *idx;
344 chars.next();
345 } else {
346 break;
347 }
348 }
349
350 if start == end {
351 return None;
352 }
353
354 let ident = &s[start..end];
355
356 let first = ident.chars().next()?;
358 if first.is_ascii_digit() {
359 return None;
360 }
361
362 if is_valid_javascript_identifier(ident) { Some(ident) } else { None }
363}
364
365#[cfg(test)]
366mod tests {
367 use std::sync::Arc;
368
369 use crate::SourceMap;
370
371 use super::*;
372
373 #[test]
376 fn test_empty_source() {
377 let view = SourceView::from_string(String::new());
378 assert_eq!(view.line_count(), 0);
379 assert_eq!(view.get_line(0), None);
380 }
381
382 #[test]
383 fn test_single_line_no_newline() {
384 let view = SourceView::from_string("hello world".into());
385 assert_eq!(view.line_count(), 1);
386 assert_eq!(view.get_line(0), Some("hello world"));
387 assert_eq!(view.get_line(1), None);
388 }
389
390 #[test]
391 fn test_single_line_with_trailing_lf() {
392 let view = SourceView::from_string("hello\n".into());
393 assert_eq!(view.line_count(), 1);
394 assert_eq!(view.get_line(0), Some("hello"));
395 }
396
397 #[test]
398 fn test_multiple_lines_lf() {
399 let view = SourceView::from_string("line1\nline2\nline3".into());
400 assert_eq!(view.line_count(), 3);
401 assert_eq!(view.get_line(0), Some("line1"));
402 assert_eq!(view.get_line(1), Some("line2"));
403 assert_eq!(view.get_line(2), Some("line3"));
404 }
405
406 #[test]
407 fn test_multiple_lines_cr() {
408 let view = SourceView::from_string("line1\rline2\rline3".into());
409 assert_eq!(view.line_count(), 3);
410 assert_eq!(view.get_line(0), Some("line1"));
411 assert_eq!(view.get_line(1), Some("line2"));
412 assert_eq!(view.get_line(2), Some("line3"));
413 }
414
415 #[test]
416 fn test_multiple_lines_crlf() {
417 let view = SourceView::from_string("line1\r\nline2\r\nline3".into());
418 assert_eq!(view.line_count(), 3);
419 assert_eq!(view.get_line(0), Some("line1"));
420 assert_eq!(view.get_line(1), Some("line2"));
421 assert_eq!(view.get_line(2), Some("line3"));
422 }
423
424 #[test]
425 fn test_mixed_line_endings() {
426 let view = SourceView::from_string("a\nb\rc\r\nd".into());
427 assert_eq!(view.line_count(), 4);
428 assert_eq!(view.get_line(0), Some("a"));
429 assert_eq!(view.get_line(1), Some("b"));
430 assert_eq!(view.get_line(2), Some("c"));
431 assert_eq!(view.get_line(3), Some("d"));
432 }
433
434 #[test]
435 fn test_empty_lines() {
436 let view = SourceView::from_string("\n\n\n".into());
437 assert_eq!(view.line_count(), 3);
438 assert_eq!(view.get_line(0), Some(""));
439 assert_eq!(view.get_line(1), Some(""));
440 assert_eq!(view.get_line(2), Some(""));
441 }
442
443 #[test]
444 fn test_crlf_trailing() {
445 let view = SourceView::from_string("a\r\n".into());
446 assert_eq!(view.line_count(), 1);
447 assert_eq!(view.get_line(0), Some("a"));
448 }
449
450 #[test]
451 fn test_cr_trailing() {
452 let view = SourceView::from_string("a\r".into());
453 assert_eq!(view.line_count(), 1);
454 assert_eq!(view.get_line(0), Some("a"));
455 }
456
457 #[test]
460 fn test_get_line_slice_ascii() {
461 let view = SourceView::from_string("abcdefgh".into());
462 assert_eq!(view.get_line_slice(0, 2, 3), Some("cde"));
463 assert_eq!(view.get_line_slice(0, 0, 8), Some("abcdefgh"));
464 assert_eq!(view.get_line_slice(0, 0, 0), Some(""));
465 }
466
467 #[test]
468 fn test_get_line_slice_multibyte() {
469 let view = SourceView::from_string("\u{00e9}\u{00e8}\u{00ea}abc".into());
471 assert_eq!(view.get_line_slice(0, 0, 3), Some("\u{00e9}\u{00e8}\u{00ea}"));
473 assert_eq!(view.get_line_slice(0, 3, 3), Some("abc"));
474 }
475
476 #[test]
477 fn test_get_line_slice_emoji_surrogate_pair() {
478 let view = SourceView::from_string("a\u{1F600}b".into());
480 assert_eq!(view.get_line_slice(0, 0, 1), Some("a"));
482 assert_eq!(view.get_line_slice(0, 1, 2), Some("\u{1F600}"));
483 assert_eq!(view.get_line_slice(0, 3, 1), Some("b"));
484 assert_eq!(view.get_line_slice(0, 0, 4), Some("a\u{1F600}b"));
485 }
486
487 #[test]
488 fn test_get_line_slice_surrogate_pair_middle() {
489 let view = SourceView::from_string("\u{1F600}".into());
491 assert_eq!(view.get_line_slice(0, 1, 1), None); }
493
494 #[test]
495 fn test_get_line_slice_out_of_bounds() {
496 let view = SourceView::from_string("abc".into());
497 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); }
501
502 #[test]
503 fn test_get_line_slice_cjk() {
504 let view = SourceView::from_string("x\u{4e16}\u{754c}y".into());
506 assert_eq!(view.get_line_slice(0, 1, 2), Some("\u{4e16}\u{754c}"));
507 }
508
509 #[test]
510 fn test_get_line_slice_multiline() {
511 let view = SourceView::from_string("abc\ndef\nghi".into());
512 assert_eq!(view.get_line_slice(0, 1, 2), Some("bc"));
513 assert_eq!(view.get_line_slice(1, 0, 3), Some("def"));
514 assert_eq!(view.get_line_slice(2, 2, 1), Some("i"));
515 }
516
517 #[test]
520 fn test_utf16_col_to_byte_offset_ascii() {
521 assert_eq!(utf16_col_to_byte_offset("abcd", 0), Some(0));
522 assert_eq!(utf16_col_to_byte_offset("abcd", 2), Some(2));
523 assert_eq!(utf16_col_to_byte_offset("abcd", 4), Some(4));
524 }
525
526 #[test]
527 fn test_utf16_col_to_byte_offset_multibyte() {
528 let s = "\u{00e9}a";
530 assert_eq!(utf16_col_to_byte_offset(s, 0), Some(0));
531 assert_eq!(utf16_col_to_byte_offset(s, 1), Some(2)); assert_eq!(utf16_col_to_byte_offset(s, 2), Some(3)); }
534
535 #[test]
536 fn test_utf16_col_to_byte_offset_surrogate_pair() {
537 let s = "\u{1F600}a";
539 assert_eq!(utf16_col_to_byte_offset(s, 0), Some(0));
540 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)); }
544
545 #[test]
546 fn test_byte_offset_to_utf16_col() {
547 assert_eq!(byte_offset_to_utf16_col("abcd", 0), 0);
548 assert_eq!(byte_offset_to_utf16_col("abcd", 2), 2);
549 let s = "a\u{1F600}b";
551 assert_eq!(byte_offset_to_utf16_col(s, 0), 0);
552 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); }
556
557 #[test]
560 fn test_extract_function_call() {
561 assert_eq!(extract_function_name_candidate("foo("), Some("foo"));
562 assert_eq!(extract_function_name_candidate(" bar("), Some("bar"));
563 assert_eq!(extract_function_name_candidate("obj.method("), Some("method"));
564 }
565
566 #[test]
567 fn test_extract_assignment() {
568 assert_eq!(extract_function_name_candidate("x ="), Some("x"));
569 assert_eq!(extract_function_name_candidate("myVar ="), Some("myVar"));
570 assert_eq!(extract_function_name_candidate("x = "), Some("x"));
571 }
572
573 #[test]
574 fn test_extract_colon() {
575 assert_eq!(extract_function_name_candidate("key:"), Some("key"));
576 assert_eq!(extract_function_name_candidate(" prop:"), Some("prop"));
577 }
578
579 #[test]
580 fn test_extract_comparison_operators() {
581 assert_eq!(extract_function_name_candidate("x =="), None);
583 assert_eq!(extract_function_name_candidate("x !="), None);
584 assert_eq!(extract_function_name_candidate("x >="), None);
585 assert_eq!(extract_function_name_candidate("x <="), None);
586 }
587
588 #[test]
589 fn test_extract_member_access() {
590 assert_eq!(extract_function_name_candidate("obj.prop"), Some("prop"));
591 assert_eq!(
592 extract_function_name_candidate("window.addEventListener"),
593 Some("addEventListener")
594 );
595 }
596
597 #[test]
598 fn test_extract_variable_declaration() {
599 assert_eq!(extract_function_name_candidate("var x"), Some("x"));
600 assert_eq!(extract_function_name_candidate("let myVar"), Some("myVar"));
601 assert_eq!(extract_function_name_candidate("const CONSTANT"), Some("CONSTANT"));
602 }
603
604 #[test]
605 fn test_extract_none() {
606 assert_eq!(extract_function_name_candidate(""), None);
607 assert_eq!(extract_function_name_candidate(" "), None);
608 assert_eq!(extract_function_name_candidate("123"), None);
609 }
610
611 #[test]
612 fn test_extract_comma_separated() {
613 assert_eq!(extract_function_name_candidate("foo(a,"), Some("a"));
614 }
615
616 #[test]
619 fn test_arc_construction() {
620 let source: Arc<str> = Arc::from("test source");
621 let view = SourceView::new(Arc::clone(&source));
622 assert_eq!(view.source(), "test source");
623 }
624
625 #[test]
626 fn test_send_sync() {
627 fn assert_send_sync<T: Send + Sync>() {}
628 assert_send_sync::<SourceView>();
629 }
630
631 #[test]
632 fn test_clone() {
633 let view = SourceView::from_string("line1\nline2".into());
634 assert_eq!(view.line_count(), 2);
636 let view2 = view.clone();
637 assert_eq!(view.get_line(1), Some("line2"));
638 assert_eq!(view2.line_count(), 2);
639 assert_eq!(view2.get_line(0), Some("line1"));
640 }
641
642 #[test]
645 fn test_get_original_function_name() {
646 let json = r#"{
649 "version": 3,
650 "sources": ["input.js"],
651 "names": ["originalFunc", "originalArg"],
652 "mappings": "AAAA,CAAC"
653 }"#;
654
655 let sm = SourceMap::from_json(json).unwrap();
656
657 let view = SourceView::from_string("a(b)".into());
664 let token = OriginalLocation { source: 0, line: 0, column: 0, name: Some(0) };
665 let result = view.get_original_function_name(&token, "nonexistent", &sm);
667 assert_eq!(result, None);
668 }
669
670 #[test]
671 fn test_get_original_function_name_with_match() {
672 let json = r#"{
684 "version": 3,
685 "sources": ["input.js"],
686 "names": ["originalFunc", "originalArg"],
687 "mappings": "AAAAA,EAAKC"
688 }"#;
689
690 let sm = SourceMap::from_json(json).unwrap();
691
692 let loc0 = sm.original_position_for(0, 0).unwrap();
694 assert_eq!(loc0.source, 0);
695 assert_eq!(loc0.line, 0);
696 assert_eq!(loc0.column, 0);
697 assert_eq!(loc0.name, Some(0));
698
699 let loc2 = sm.original_position_for(0, 2).unwrap();
700 assert_eq!(loc2.source, 0);
701 assert_eq!(loc2.line, 0);
702 assert_eq!(loc2.column, 5);
703 assert_eq!(loc2.name, Some(1));
704
705 let view = SourceView::from_string("a(b)".into());
707
708 let token = OriginalLocation { source: 0, line: 0, column: 5, name: Some(1) };
711
712 let result = view.get_original_function_name(&token, "a", &sm);
714 assert_eq!(result, Some("originalFunc"));
715 }
716
717 #[test]
718 fn test_line_cache_consistency() {
719 let view = SourceView::from_string("a\nb\nc".into());
720 assert_eq!(view.get_line(2), Some("c"));
722 assert_eq!(view.get_line(0), Some("a"));
723 assert_eq!(view.get_line(1), Some("b"));
724 assert_eq!(view.line_count(), 3);
725 }
726
727 #[test]
728 fn test_only_newlines() {
729 let view = SourceView::from_string("\n".into());
730 assert_eq!(view.line_count(), 1);
731 assert_eq!(view.get_line(0), Some(""));
732 }
733
734 #[test]
735 fn test_consecutive_crlf() {
736 let view = SourceView::from_string("\r\n\r\n".into());
737 assert_eq!(view.line_count(), 2);
738 assert_eq!(view.get_line(0), Some(""));
739 assert_eq!(view.get_line(1), Some(""));
740 }
741
742 #[test]
743 fn test_unicode_line_content() {
744 let view = SourceView::from_string("Hello \u{4e16}\u{754c}\n\u{1F600} smile".into());
745 assert_eq!(view.line_count(), 2);
746 assert_eq!(view.get_line(0), Some("Hello \u{4e16}\u{754c}"));
747 assert_eq!(view.get_line(1), Some("\u{1F600} smile"));
748 }
749
750 #[test]
751 fn test_get_line_slice_at_line_end() {
752 let view = SourceView::from_string("abc".into());
753 assert_eq!(view.get_line_slice(0, 3, 0), Some(""));
755 }
756
757 #[test]
758 fn test_get_line_slice_full_line() {
759 let view = SourceView::from_string("abc\ndef".into());
760 assert_eq!(view.get_line_slice(0, 0, 3), Some("abc"));
761 assert_eq!(view.get_line_slice(1, 0, 3), Some("def"));
762 }
763}