Skip to main content

srcmap_sourcemap/
source_view.rs

1//! Efficient source view with lazy line caching and UTF-16 column support.
2//!
3//! [`SourceView`] provides indexed access to lines in a JavaScript source file,
4//! with support for UTF-16 column offsets (as used by source maps) and
5//! heuristic function name inference for error grouping.
6
7use std::sync::{Arc, OnceLock};
8
9use crate::js_identifiers::is_valid_javascript_identifier;
10use crate::{OriginalLocation, SourceMap};
11
12/// Pre-computed byte offset pair for a single line.
13///
14/// `start` is inclusive, `end` is exclusive, and excludes the line terminator.
15#[derive(Debug, Clone, Copy)]
16struct LineRange {
17    start: usize,
18    end: usize,
19}
20
21/// Efficient view into a JavaScript source string with lazy line indexing.
22///
23/// Stores the source as `Arc<str>` for cheap cloning. Line boundaries are
24/// computed on first access and cached with [`OnceLock`] for lock-free
25/// concurrent reads.
26///
27/// # UTF-16 column support
28///
29/// JavaScript source maps use UTF-16 code-unit offsets for columns. Methods
30/// like [`get_line_slice`](SourceView::get_line_slice) accept UTF-16 columns
31/// and convert them to byte offsets internally.
32#[derive(Debug, Clone)]
33pub struct SourceView {
34    source: Arc<str>,
35    line_cache: OnceLock<Vec<LineRange>>,
36}
37
38impl SourceView {
39    /// Create a new `SourceView` from a shared source string.
40    pub fn new(source: Arc<str>) -> Self {
41        Self { source, line_cache: OnceLock::new() }
42    }
43
44    /// Create a new `SourceView` from an owned `String`.
45    pub fn from_string(source: String) -> Self {
46        Self::new(Arc::from(source))
47    }
48
49    /// Return the full source string.
50    #[inline]
51    pub fn source(&self) -> &str {
52        &self.source
53    }
54
55    /// Return the number of lines in the source.
56    ///
57    /// A trailing newline does NOT produce an extra empty line
58    /// (e.g. `"a\n"` has 1 line, `"a\nb"` has 2 lines).
59    pub fn line_count(&self) -> usize {
60        self.lines().len()
61    }
62
63    /// Get a specific line by 0-based index.
64    ///
65    /// Returns `None` if `idx` is out of bounds. The returned slice does NOT
66    /// include the line terminator.
67    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    /// Get a substring of a line using UTF-16 column offsets.
74    ///
75    /// `line` and `col` are 0-based. `span` is the number of UTF-16 code units
76    /// to include. Returns `None` if the line index is out of bounds or the
77    /// column/span extends past the line.
78    ///
79    /// This is necessary because JavaScript source maps encode columns as
80    /// UTF-16 code unit offsets, but Rust strings are UTF-8.
81    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    /// Attempt to infer the original function name for a token.
89    ///
90    /// This is a best-effort heuristic used for error grouping. Given a token's
91    /// position in the generated (minified) source and the minified name at
92    /// that position, it looks at surrounding code patterns to identify the
93    /// function context.
94    ///
95    /// # Algorithm
96    ///
97    /// 1. Find the mapping at the token's generated position
98    /// 2. Look backwards from that position in the generated line to find
99    ///    patterns like `name(`, `name:`, `name =`, `.name`
100    /// 3. If the identified name matches `minified_name`, look it up in the
101    ///    source map for the original name
102    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        // We need to find where this original location maps to in the generated source
109        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        // Look at code before the token position to find function name patterns
116        let prefix = &line_str[..col_byte];
117
118        // Try to find what precedes this position
119        let candidate = extract_function_name_candidate(prefix)?;
120
121        if !is_valid_javascript_identifier(candidate) {
122            return None;
123        }
124
125        // If the candidate matches the minified name, look for the original
126        // name by finding a mapping at the candidate's position
127        if candidate != minified_name {
128            return None;
129        }
130
131        // Find the byte offset where the candidate starts in the line
132        let candidate_start_byte = prefix.len() - candidate.len();
133        let candidate_col = byte_offset_to_utf16_col(line_str, candidate_start_byte);
134
135        // Look up the original location for this generated position
136        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    /// Compute or retrieve the cached line ranges.
142    fn lines(&self) -> &[LineRange] {
143        self.line_cache.get_or_init(|| compute_line_ranges(&self.source))
144    }
145}
146
147/// Compute `(start, end)` byte-offset pairs for every line.
148///
149/// Handles LF (`\n`), CR (`\r`), and CRLF (`\r\n`) terminators.
150/// A trailing newline does NOT produce an extra empty line.
151fn 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                // Skip \n in \r\n
173                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 the source doesn't end with a newline, add the last line
187    if start < len {
188        ranges.push(LineRange { start, end: len });
189    }
190
191    ranges
192}
193
194/// Convert a UTF-16 column offset to a byte offset within a UTF-8 string.
195///
196/// Returns `None` if the column is past the end of the string.
197fn 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            // Column points into the middle of a surrogate pair
210            return None;
211        }
212    }
213
214    // Column exactly at the end of the string
215    if utf16_offset == col {
216        return Some(s.len());
217    }
218
219    None
220}
221
222/// Advance `span` UTF-16 code units from `start_byte` and return the resulting byte offset.
223///
224/// Returns `None` if the span extends past the end of the string.
225fn 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
249/// Convert a byte offset to a UTF-16 column offset.
250fn 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
255/// Extract a function name candidate from the text preceding a token.
256///
257/// Looks for patterns like:
258/// - `name(` — function call or declaration
259/// - `name:` — object property
260/// - `name =` / `name=` — assignment
261/// - `.name` — member access
262/// - `var name` / `let name` / `const name` — variable declaration
263fn 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        // `name(` pattern — the prefix already has the `(` trimmed, look for ident before it
273        '(' | ',' => {
274            let before_paren = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
275            extract_trailing_identifier(before_paren)
276        }
277        // `name:` pattern
278        ':' => {
279            let before_colon = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
280            extract_trailing_identifier(before_colon)
281        }
282        // `name =` pattern
283        '=' => {
284            // Make sure it's not `==`, `!=`, `>=`, `<=`, `+=`, `-=`, etc.
285            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        // `.name` or identifier at end — try to get the trailing identifier
298        _ 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            // Keyword patterns: var/let/const
309            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
322/// Extract the trailing JavaScript identifier from a string.
323///
324/// Scans backwards from the end to find the start of the identifier.
325fn 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    // Find identifier characters from the end
334    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    // Verify it starts with a valid identifier start character
357    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    // ── Line cache tests ─────────────────────────────────────────
374
375    #[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    // ── UTF-16 column tests ─────────────────────────────────────
458
459    #[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        // Each char here is 3 bytes in UTF-8 but 1 UTF-16 code unit
470        let view = SourceView::from_string("\u{00e9}\u{00e8}\u{00ea}abc".into());
471        // UTF-16 col 0 = \u{00e9}, col 1 = \u{00e8}, col 2 = \u{00ea}, col 3 = a, etc.
472        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        // Emoji: U+1F600 (GRINNING FACE) is 4 bytes in UTF-8, 2 UTF-16 code units
479        let view = SourceView::from_string("a\u{1F600}b".into());
480        // UTF-16: col 0 = 'a' (1 unit), col 1-2 = emoji (2 units), col 3 = 'b' (1 unit)
481        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        // Pointing into the middle of a surrogate pair should return None
490        let view = SourceView::from_string("\u{1F600}".into());
491        assert_eq!(view.get_line_slice(0, 1, 1), None); // middle of surrogate pair
492    }
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); // span too long
498        assert_eq!(view.get_line_slice(0, 5, 1), None); // col past end
499        assert_eq!(view.get_line_slice(1, 0, 1), None); // line doesn't exist
500    }
501
502    #[test]
503    fn test_get_line_slice_cjk() {
504        // CJK characters: U+4E16 and U+754C are 3 bytes in UTF-8, 1 UTF-16 code unit each
505        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    // ── UTF-16 conversion tests ─────────────────────────────────
518
519    #[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        // \u{00e9} is 2 bytes in UTF-8, 1 UTF-16 code unit
529        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)); // after \u{00e9}
532        assert_eq!(utf16_col_to_byte_offset(s, 2), Some(3)); // after 'a'
533    }
534
535    #[test]
536    fn test_utf16_col_to_byte_offset_surrogate_pair() {
537        // U+1F600 is 4 bytes in UTF-8, 2 UTF-16 code units
538        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); // middle of surrogate pair
541        assert_eq!(utf16_col_to_byte_offset(s, 2), Some(4)); // after emoji
542        assert_eq!(utf16_col_to_byte_offset(s, 3), Some(5)); // after 'a'
543    }
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        // \u{1F600} is 4 bytes, 2 UTF-16 units
550        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); // after 'a'
553        assert_eq!(byte_offset_to_utf16_col(s, 5), 3); // after emoji (1 + 2)
554        assert_eq!(byte_offset_to_utf16_col(s, 6), 4); // after 'b'
555    }
556
557    // ── Function name candidate extraction tests ────────────────
558
559    #[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        // These should NOT be treated as assignments
582        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    // ── Arc / Send / Sync tests ──────────────────────────────────
617
618    #[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        // Prime the cache
635        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    // ── Integration test with SourceMap ──────────────────────────
643
644    #[test]
645    fn test_get_original_function_name() {
646        // Build a source map that maps generated positions to original positions
647        // Generated: "a(b)" where `a` was originally `originalFunc` and `b` was `originalArg`
648        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        // The generated source is "a(b)"
658        // Mapping: gen 0:0 -> orig 0:0 (source 0), gen 0:2 -> orig 0:1 (source 0)
659        // But we need names in the mappings for this to work.
660        // Let's create a more realistic test with the builder.
661
662        // For now, verify that the function works without crashing when there's no match
663        let view = SourceView::from_string("a(b)".into());
664        let token = OriginalLocation { source: 0, line: 0, column: 0, name: Some(0) };
665        // This should return None since the heuristic won't find a matching minified name
666        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        // Create a source map where:
673        // Generated line 0: "a(b)"
674        //   col 0 -> source 0, line 0, col 0, name "originalFunc" (names[0])
675        //   col 2 -> source 0, line 0, col 5, name "originalArg" (names[1])
676        //
677        // We want to test: given a token at orig 0:5 (which maps to gen 0:2),
678        // the code before gen 0:2 is "a(" — so the candidate is "a".
679        // If minified_name is "a", we look up gen 0:0 to find name "originalFunc".
680
681        // AAAAA = gen_col 0, source 0, orig_line 0, orig_col 0, name 0
682        // EAAKC = gen_col +2, source +0, orig_line +0, orig_col +5, name +1
683        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        // Verify the mappings are correct
693        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        // Generated source: "a(b)"
706        let view = SourceView::from_string("a(b)".into());
707
708        // Token is at original 0:5 with name "originalArg"
709        // We want to find the function name "originalFunc" for this token
710        let token = OriginalLocation { source: 0, line: 0, column: 5, name: Some(1) };
711
712        // The minified name "a" should match the candidate extracted from "a("
713        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        // Access lines in different orders to ensure cache is consistent
721        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        // Slice at the very end with 0 span
754        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}