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 {
42            source,
43            line_cache: OnceLock::new(),
44        }
45    }
46
47    /// Create a new `SourceView` from an owned `String`.
48    pub fn from_string(source: String) -> Self {
49        Self::new(Arc::from(source))
50    }
51
52    /// Return the full source string.
53    #[inline]
54    pub fn source(&self) -> &str {
55        &self.source
56    }
57
58    /// Return the number of lines in the source.
59    ///
60    /// A trailing newline does NOT produce an extra empty line
61    /// (e.g. `"a\n"` has 1 line, `"a\nb"` has 2 lines).
62    pub fn line_count(&self) -> usize {
63        self.lines().len()
64    }
65
66    /// Get a specific line by 0-based index.
67    ///
68    /// Returns `None` if `idx` is out of bounds. The returned slice does NOT
69    /// include the line terminator.
70    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    /// Get a substring of a line using UTF-16 column offsets.
77    ///
78    /// `line` and `col` are 0-based. `span` is the number of UTF-16 code units
79    /// to include. Returns `None` if the line index is out of bounds or the
80    /// column/span extends past the line.
81    ///
82    /// This is necessary because JavaScript source maps encode columns as
83    /// UTF-16 code unit offsets, but Rust strings are UTF-8.
84    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    /// Attempt to infer the original function name for a token.
92    ///
93    /// This is a best-effort heuristic used for error grouping. Given a token's
94    /// position in the generated (minified) source and the minified name at
95    /// that position, it looks at surrounding code patterns to identify the
96    /// function context.
97    ///
98    /// # Algorithm
99    ///
100    /// 1. Find the mapping at the token's generated position
101    /// 2. Look backwards from that position in the generated line to find
102    ///    patterns like `name(`, `name:`, `name =`, `.name`
103    /// 3. If the identified name matches `minified_name`, look it up in the
104    ///    source map for the original name
105    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        // We need to find where this original location maps to in the generated source
112        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        // Look at code before the token position to find function name patterns
119        let prefix = &line_str[..col_byte];
120
121        // Try to find what precedes this position
122        let candidate = extract_function_name_candidate(prefix)?;
123
124        if !is_valid_javascript_identifier(candidate) {
125            return None;
126        }
127
128        // If the candidate matches the minified name, look for the original
129        // name by finding a mapping at the candidate's position
130        if candidate != minified_name {
131            return None;
132        }
133
134        // Find the byte offset where the candidate starts in the line
135        let candidate_start_byte = prefix.len() - candidate.len();
136        let candidate_col = byte_offset_to_utf16_col(line_str, candidate_start_byte);
137
138        // Look up the original location for this generated position
139        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    /// Compute or retrieve the cached line ranges.
145    fn lines(&self) -> &[LineRange] {
146        self.line_cache
147            .get_or_init(|| compute_line_ranges(&self.source))
148    }
149}
150
151/// Compute `(start, end)` byte-offset pairs for every line.
152///
153/// Handles LF (`\n`), CR (`\r`), and CRLF (`\r\n`) terminators.
154/// A trailing newline does NOT produce an extra empty line.
155fn 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                // Skip \n in \r\n
177                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 the source doesn't end with a newline, add the last line
191    if start < len {
192        ranges.push(LineRange { start, end: len });
193    }
194
195    ranges
196}
197
198/// Convert a UTF-16 column offset to a byte offset within a UTF-8 string.
199///
200/// Returns `None` if the column is past the end of the string.
201fn 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            // Column points into the middle of a surrogate pair
214            return None;
215        }
216    }
217
218    // Column exactly at the end of the string
219    if utf16_offset == col {
220        return Some(s.len());
221    }
222
223    None
224}
225
226/// Advance `span` UTF-16 code units from `start_byte` and return the resulting byte offset.
227///
228/// Returns `None` if the span extends past the end of the string.
229fn 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
253/// Convert a byte offset to a UTF-16 column offset.
254fn 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
259/// Extract a function name candidate from the text preceding a token.
260///
261/// Looks for patterns like:
262/// - `name(` — function call or declaration
263/// - `name:` — object property
264/// - `name =` / `name=` — assignment
265/// - `.name` — member access
266/// - `var name` / `let name` / `const name` — variable declaration
267fn 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        // `name(` pattern — the prefix already has the `(` trimmed, look for ident before it
277        '(' | ',' => {
278            let before_paren = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
279            extract_trailing_identifier(before_paren)
280        }
281        // `name:` pattern
282        ':' => {
283            let before_colon = trimmed[..trimmed.len() - last_char.len_utf8()].trim_end();
284            extract_trailing_identifier(before_colon)
285        }
286        // `name =` pattern
287        '=' => {
288            // Make sure it's not `==`, `!=`, `>=`, `<=`, `+=`, `-=`, etc.
289            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        // `.name` or identifier at end — try to get the trailing identifier
302        _ 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            // Keyword patterns: var/let/const
313            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
326/// Extract the trailing JavaScript identifier from a string.
327///
328/// Scans backwards from the end to find the start of the identifier.
329fn 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    // Find identifier characters from the end
338    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    // Verify it starts with a valid identifier start character
361    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    // ── Line cache tests ─────────────────────────────────────────
382
383    #[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    // ── UTF-16 column tests ─────────────────────────────────────
466
467    #[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        // Each char here is 3 bytes in UTF-8 but 1 UTF-16 code unit
478        let view = SourceView::from_string("\u{00e9}\u{00e8}\u{00ea}abc".into());
479        // UTF-16 col 0 = \u{00e9}, col 1 = \u{00e8}, col 2 = \u{00ea}, col 3 = a, etc.
480        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        // Emoji: U+1F600 (GRINNING FACE) is 4 bytes in UTF-8, 2 UTF-16 code units
490        let view = SourceView::from_string("a\u{1F600}b".into());
491        // UTF-16: col 0 = 'a' (1 unit), col 1-2 = emoji (2 units), col 3 = 'b' (1 unit)
492        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        // Pointing into the middle of a surrogate pair should return None
501        let view = SourceView::from_string("\u{1F600}".into());
502        assert_eq!(view.get_line_slice(0, 1, 1), None); // middle of surrogate pair
503    }
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); // span too long
509        assert_eq!(view.get_line_slice(0, 5, 1), None); // col past end
510        assert_eq!(view.get_line_slice(1, 0, 1), None); // line doesn't exist
511    }
512
513    #[test]
514    fn test_get_line_slice_cjk() {
515        // CJK characters: U+4E16 and U+754C are 3 bytes in UTF-8, 1 UTF-16 code unit each
516        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    // ── UTF-16 conversion tests ─────────────────────────────────
529
530    #[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        // \u{00e9} is 2 bytes in UTF-8, 1 UTF-16 code unit
540        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)); // after \u{00e9}
543        assert_eq!(utf16_col_to_byte_offset(s, 2), Some(3)); // after 'a'
544    }
545
546    #[test]
547    fn test_utf16_col_to_byte_offset_surrogate_pair() {
548        // U+1F600 is 4 bytes in UTF-8, 2 UTF-16 code units
549        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); // middle of surrogate pair
552        assert_eq!(utf16_col_to_byte_offset(s, 2), Some(4)); // after emoji
553        assert_eq!(utf16_col_to_byte_offset(s, 3), Some(5)); // after 'a'
554    }
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        // \u{1F600} is 4 bytes, 2 UTF-16 units
561        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); // after 'a'
564        assert_eq!(byte_offset_to_utf16_col(s, 5), 3); // after emoji (1 + 2)
565        assert_eq!(byte_offset_to_utf16_col(s, 6), 4); // after 'b'
566    }
567
568    // ── Function name candidate extraction tests ────────────────
569
570    #[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        // These should NOT be treated as assignments
596        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    // ── Arc / Send / Sync tests ──────────────────────────────────
634
635    #[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        // Prime the cache
652        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    // ── Integration test with SourceMap ──────────────────────────
659
660    #[test]
661    fn test_get_original_function_name() {
662        // Build a source map that maps generated positions to original positions
663        // Generated: "a(b)" where `a` was originally `originalFunc` and `b` was `originalArg`
664        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        // The generated source is "a(b)"
674        // Mapping: gen 0:0 -> orig 0:0 (source 0), gen 0:2 -> orig 0:1 (source 0)
675        // But we need names in the mappings for this to work.
676        // Let's create a more realistic test with the builder.
677
678        // For now, verify that the function works without crashing when there's no match
679        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        // This should return None since the heuristic won't find a matching minified name
687        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        // Create a source map where:
694        // Generated line 0: "a(b)"
695        //   col 0 -> source 0, line 0, col 0, name "originalFunc" (names[0])
696        //   col 2 -> source 0, line 0, col 5, name "originalArg" (names[1])
697        //
698        // We want to test: given a token at orig 0:5 (which maps to gen 0:2),
699        // the code before gen 0:2 is "a(" — so the candidate is "a".
700        // If minified_name is "a", we look up gen 0:0 to find name "originalFunc".
701
702        // AAAAA = gen_col 0, source 0, orig_line 0, orig_col 0, name 0
703        // EAAKC = gen_col +2, source +0, orig_line +0, orig_col +5, name +1
704        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        // Verify the mappings are correct
714        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        // Generated source: "a(b)"
727        let view = SourceView::from_string("a(b)".into());
728
729        // Token is at original 0:5 with name "originalArg"
730        // We want to find the function name "originalFunc" for this token
731        let token = OriginalLocation {
732            source: 0,
733            line: 0,
734            column: 5,
735            name: Some(1),
736        };
737
738        // The minified name "a" should match the candidate extracted from "a("
739        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        // Access lines in different orders to ensure cache is consistent
747        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        // Slice at the very end with 0 span
780        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}