Skip to main content

srcmap_sourcemap/
lib.rs

1//! High-performance source map parser and consumer (ECMA-426).
2//!
3//! Parses source map JSON and provides O(log n) position lookups.
4//! Uses a flat, cache-friendly representation internally.
5//!
6//! # Examples
7//!
8//! ```
9//! use srcmap_sourcemap::SourceMap;
10//!
11//! let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
12//! let sm = SourceMap::from_json(json).unwrap();
13//!
14//! // Look up original position for generated line 0, column 0
15//! let loc = sm.original_position_for(0, 0).unwrap();
16//! assert_eq!(sm.source(loc.source), "input.js");
17//! assert_eq!(loc.line, 0);
18//! assert_eq!(loc.column, 0);
19//!
20//! // Reverse lookup
21//! let pos = sm.generated_position_for("input.js", 0, 0).unwrap();
22//! assert_eq!(pos.line, 0);
23//! assert_eq!(pos.column, 0);
24//! ```
25
26use std::cell::OnceCell;
27use std::collections::HashMap;
28use std::fmt;
29
30use serde::Deserialize;
31use srcmap_codec::DecodeError;
32
33// ── Constants ──────────────────────────────────────────────────────
34
35const NO_SOURCE: u32 = u32::MAX;
36const NO_NAME: u32 = u32::MAX;
37
38// ── Public types ───────────────────────────────────────────────────
39
40/// A single decoded mapping entry. Compact at 24 bytes (6 × u32).
41#[derive(Debug, Clone, Copy)]
42pub struct Mapping {
43    pub generated_line: u32,
44    pub generated_column: u32,
45    /// Index into `SourceMap::sources`. `u32::MAX` if absent.
46    pub source: u32,
47    pub original_line: u32,
48    pub original_column: u32,
49    /// Index into `SourceMap::names`. `u32::MAX` if absent.
50    pub name: u32,
51}
52
53/// Result of an `original_position_for` lookup.
54#[derive(Debug, Clone)]
55pub struct OriginalLocation {
56    pub source: u32,
57    pub line: u32,
58    pub column: u32,
59    pub name: Option<u32>,
60}
61
62/// Result of a `generated_position_for` lookup.
63#[derive(Debug, Clone)]
64pub struct GeneratedLocation {
65    pub line: u32,
66    pub column: u32,
67}
68
69/// Errors during source map parsing.
70#[derive(Debug)]
71pub enum ParseError {
72    Json(serde_json::Error),
73    Vlq(DecodeError),
74    InvalidVersion(u32),
75}
76
77impl fmt::Display for ParseError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            Self::Json(e) => write!(f, "JSON parse error: {e}"),
81            Self::Vlq(e) => write!(f, "VLQ decode error: {e}"),
82            Self::InvalidVersion(v) => write!(f, "unsupported source map version: {v}"),
83        }
84    }
85}
86
87impl std::error::Error for ParseError {}
88
89impl From<serde_json::Error> for ParseError {
90    fn from(e: serde_json::Error) -> Self {
91        Self::Json(e)
92    }
93}
94
95impl From<DecodeError> for ParseError {
96    fn from(e: DecodeError) -> Self {
97        Self::Vlq(e)
98    }
99}
100
101// ── Raw JSON structure ─────────────────────────────────────────────
102
103#[derive(Deserialize)]
104struct RawSourceMap<'a> {
105    version: u32,
106    #[serde(default)]
107    file: Option<String>,
108    #[serde(default, rename = "sourceRoot")]
109    source_root: Option<String>,
110    #[serde(default)]
111    sources: Vec<Option<String>>,
112    #[serde(default, rename = "sourcesContent")]
113    sources_content: Option<Vec<Option<String>>>,
114    #[serde(default)]
115    names: Vec<String>,
116    #[serde(default, borrow)]
117    mappings: &'a str,
118    #[serde(default, rename = "ignoreList")]
119    ignore_list: Vec<u32>,
120    /// Indexed source maps use `sections` instead of `mappings`.
121    #[serde(default)]
122    sections: Option<Vec<RawSection>>,
123}
124
125/// A section in an indexed source map.
126#[derive(Deserialize)]
127struct RawSection {
128    offset: RawOffset,
129    map: serde_json::Value,
130}
131
132#[derive(Deserialize)]
133struct RawOffset {
134    line: u32,
135    column: u32,
136}
137
138// ── SourceMap ──────────────────────────────────────────────────────
139
140/// A parsed source map with O(log n) position lookups.
141#[derive(Debug, Clone)]
142pub struct SourceMap {
143    pub file: Option<String>,
144    pub source_root: Option<String>,
145    pub sources: Vec<String>,
146    pub sources_content: Vec<Option<String>>,
147    pub names: Vec<String>,
148    pub ignore_list: Vec<u32>,
149
150    /// Flat decoded mappings, ordered by (generated_line, generated_column).
151    mappings: Vec<Mapping>,
152
153    /// `line_offsets[i]` = index of first mapping on generated line `i`.
154    /// `line_offsets[line_count]` = mappings.len() (sentinel).
155    line_offsets: Vec<u32>,
156
157    /// Indices into `mappings`, sorted by (source, original_line, original_column).
158    /// Built lazily on first `generated_position_for` call.
159    reverse_index: OnceCell<Vec<u32>>,
160
161    /// Source filename → index for O(1) lookup by name.
162    source_map: HashMap<String, u32>,
163}
164
165impl SourceMap {
166    /// Parse a source map from a JSON string.
167    /// Supports both regular and indexed (sectioned) source maps.
168    pub fn from_json(json: &str) -> Result<Self, ParseError> {
169        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
170
171        if raw.version != 3 {
172            return Err(ParseError::InvalidVersion(raw.version));
173        }
174
175        // Handle indexed source maps (sections)
176        if let Some(sections) = raw.sections {
177            return Self::from_sections(raw.file, sections);
178        }
179
180        Self::from_regular(raw)
181    }
182
183    /// Parse a regular (non-indexed) source map.
184    fn from_regular(raw: RawSourceMap<'_>) -> Result<Self, ParseError> {
185        // Resolve sources: apply sourceRoot, replace None with empty string
186        let source_root = raw.source_root.as_deref().unwrap_or("");
187        let sources: Vec<String> = raw
188            .sources
189            .iter()
190            .map(|s| match s {
191                Some(s) if !source_root.is_empty() => format!("{source_root}{s}"),
192                Some(s) => s.clone(),
193                None => String::new(),
194            })
195            .collect();
196
197        let sources_content = raw.sources_content.unwrap_or_default();
198
199        // Build source name → index map
200        let source_map: HashMap<String, u32> = sources
201            .iter()
202            .enumerate()
203            .map(|(i, s)| (s.clone(), i as u32))
204            .collect();
205
206        // Decode mappings directly into flat Mapping vec
207        let (mappings, line_offsets) = decode_mappings(raw.mappings)?;
208
209        Ok(Self {
210            file: raw.file,
211            source_root: raw.source_root,
212            sources,
213            sources_content,
214            names: raw.names,
215            ignore_list: raw.ignore_list,
216            mappings,
217            line_offsets,
218            reverse_index: OnceCell::new(),
219            source_map,
220        })
221    }
222
223    /// Flatten an indexed source map (with sections) into a regular one.
224    fn from_sections(file: Option<String>, sections: Vec<RawSection>) -> Result<Self, ParseError> {
225        let mut all_sources: Vec<String> = Vec::new();
226        let mut all_sources_content: Vec<Option<String>> = Vec::new();
227        let mut all_names: Vec<String> = Vec::new();
228        let mut all_mappings: Vec<Mapping> = Vec::new();
229        let mut all_ignore_list: Vec<u32> = Vec::new();
230        let mut max_line: u32 = 0;
231
232        // Source/name dedup maps to merge across sections
233        let mut source_index_map: HashMap<String, u32> = HashMap::new();
234        let mut name_index_map: HashMap<String, u32> = HashMap::new();
235
236        for section in &sections {
237            let section_json = serde_json::to_string(&section.map).map_err(ParseError::Json)?;
238            let sub = Self::from_json(&section_json)?;
239
240            let line_offset = section.offset.line;
241            let col_offset = section.offset.column;
242
243            // Map section source indices to global indices
244            let source_remap: Vec<u32> = sub
245                .sources
246                .iter()
247                .enumerate()
248                .map(|(i, s)| {
249                    if let Some(&existing) = source_index_map.get(s) {
250                        existing
251                    } else {
252                        let idx = all_sources.len() as u32;
253                        all_sources.push(s.clone());
254                        // Add sourcesContent if available
255                        let content = sub.sources_content.get(i).cloned().unwrap_or(None);
256                        all_sources_content.push(content);
257                        source_index_map.insert(s.clone(), idx);
258                        idx
259                    }
260                })
261                .collect();
262
263            // Map section name indices to global indices
264            let name_remap: Vec<u32> = sub
265                .names
266                .iter()
267                .map(|n| {
268                    if let Some(&existing) = name_index_map.get(n) {
269                        existing
270                    } else {
271                        let idx = all_names.len() as u32;
272                        all_names.push(n.clone());
273                        name_index_map.insert(n.clone(), idx);
274                        idx
275                    }
276                })
277                .collect();
278
279            // Add ignore_list entries (remapped to global source indices)
280            for &idx in &sub.ignore_list {
281                let global_idx = source_remap[idx as usize];
282                if !all_ignore_list.contains(&global_idx) {
283                    all_ignore_list.push(global_idx);
284                }
285            }
286
287            // Remap and offset all mappings from this section
288            for m in &sub.mappings {
289                let gen_line = m.generated_line + line_offset;
290                let gen_col = if m.generated_line == 0 {
291                    m.generated_column + col_offset
292                } else {
293                    m.generated_column
294                };
295
296                all_mappings.push(Mapping {
297                    generated_line: gen_line,
298                    generated_column: gen_col,
299                    source: if m.source == NO_SOURCE {
300                        NO_SOURCE
301                    } else {
302                        source_remap[m.source as usize]
303                    },
304                    original_line: m.original_line,
305                    original_column: m.original_column,
306                    name: if m.name == NO_NAME {
307                        NO_NAME
308                    } else {
309                        name_remap[m.name as usize]
310                    },
311                });
312
313                if gen_line > max_line {
314                    max_line = gen_line;
315                }
316            }
317        }
318
319        // Sort mappings by (generated_line, generated_column)
320        all_mappings.sort_unstable_by(|a, b| {
321            a.generated_line
322                .cmp(&b.generated_line)
323                .then(a.generated_column.cmp(&b.generated_column))
324        });
325
326        // Build line_offsets
327        let line_count = if all_mappings.is_empty() {
328            0
329        } else {
330            max_line as usize + 1
331        };
332        let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
333        let mut current_line: usize = 0;
334        for (i, m) in all_mappings.iter().enumerate() {
335            while current_line < m.generated_line as usize {
336                current_line += 1;
337                if current_line < line_offsets.len() {
338                    line_offsets[current_line] = i as u32;
339                }
340            }
341        }
342        // Fill sentinel
343        if !line_offsets.is_empty() {
344            let last = all_mappings.len() as u32;
345            for offset in line_offsets.iter_mut().skip(current_line + 1) {
346                *offset = last;
347            }
348        }
349
350        Ok(Self {
351            file,
352            source_root: None,
353            sources: all_sources.clone(),
354            sources_content: all_sources_content,
355            names: all_names,
356            ignore_list: all_ignore_list,
357            mappings: all_mappings,
358            line_offsets,
359            reverse_index: OnceCell::new(),
360            source_map: all_sources
361                .into_iter()
362                .enumerate()
363                .map(|(i, s)| (s, i as u32))
364                .collect(),
365        })
366    }
367
368    /// Look up the original source position for a generated position.
369    ///
370    /// Both `line` and `column` are 0-based.
371    /// Returns `None` if no mapping exists or the mapping has no source.
372    pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
373        let line_idx = line as usize;
374        if line_idx + 1 >= self.line_offsets.len() {
375            return None;
376        }
377
378        let start = self.line_offsets[line_idx] as usize;
379        let end = self.line_offsets[line_idx + 1] as usize;
380
381        if start == end {
382            return None;
383        }
384
385        let line_mappings = &self.mappings[start..end];
386
387        // Binary search: find largest generated_column <= column
388        let idx = match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
389            Ok(i) => i,
390            Err(0) => return None,
391            Err(i) => i - 1,
392        };
393
394        let mapping = &line_mappings[idx];
395
396        if mapping.source == NO_SOURCE {
397            return None;
398        }
399
400        Some(OriginalLocation {
401            source: mapping.source,
402            line: mapping.original_line,
403            column: mapping.original_column,
404            name: if mapping.name == NO_NAME {
405                None
406            } else {
407                Some(mapping.name)
408            },
409        })
410    }
411
412    /// Look up the generated position for an original source position.
413    ///
414    /// `source` is the source filename. `line` and `column` are 0-based.
415    pub fn generated_position_for(
416        &self,
417        source: &str,
418        line: u32,
419        column: u32,
420    ) -> Option<GeneratedLocation> {
421        let &source_idx = self.source_map.get(source)?;
422
423        let reverse_index = self
424            .reverse_index
425            .get_or_init(|| build_reverse_index(&self.mappings));
426
427        // Binary search in reverse_index for (source, line, column)
428        let idx = reverse_index.partition_point(|&i| {
429            let m = &self.mappings[i as usize];
430            (m.source, m.original_line, m.original_column) < (source_idx, line, column)
431        });
432
433        if idx >= reverse_index.len() {
434            return None;
435        }
436
437        let mapping = &self.mappings[reverse_index[idx] as usize];
438
439        if mapping.source != source_idx {
440            return None;
441        }
442
443        Some(GeneratedLocation {
444            line: mapping.generated_line,
445            column: mapping.generated_column,
446        })
447    }
448
449    /// Resolve a source index to its filename.
450    pub fn source(&self, index: u32) -> &str {
451        &self.sources[index as usize]
452    }
453
454    /// Resolve a name index to its string.
455    pub fn name(&self, index: u32) -> &str {
456        &self.names[index as usize]
457    }
458
459    /// Find the source index for a filename.
460    pub fn source_index(&self, name: &str) -> Option<u32> {
461        self.source_map.get(name).copied()
462    }
463
464    /// Total number of decoded mappings.
465    pub fn mapping_count(&self) -> usize {
466        self.mappings.len()
467    }
468
469    /// Number of generated lines.
470    pub fn line_count(&self) -> usize {
471        self.line_offsets.len().saturating_sub(1)
472    }
473
474    /// Get all mappings for a generated line (0-based).
475    pub fn mappings_for_line(&self, line: u32) -> &[Mapping] {
476        let line_idx = line as usize;
477        if line_idx + 1 >= self.line_offsets.len() {
478            return &[];
479        }
480        let start = self.line_offsets[line_idx] as usize;
481        let end = self.line_offsets[line_idx + 1] as usize;
482        &self.mappings[start..end]
483    }
484
485    /// Iterate all mappings.
486    pub fn all_mappings(&self) -> &[Mapping] {
487        &self.mappings
488    }
489
490    /// Serialize the source map back to JSON.
491    ///
492    /// Produces a valid source map v3 JSON string that can be written to a file
493    /// or embedded in a data URL.
494    pub fn to_json(&self) -> String {
495        let mappings = self.encode_mappings();
496
497        let mut json = String::with_capacity(256 + mappings.len());
498        json.push_str(r#"{"version":3"#);
499
500        if let Some(ref file) = self.file {
501            json.push_str(r#","file":"#);
502            json_quote_into(&mut json, file);
503        }
504
505        if let Some(ref root) = self.source_root {
506            json.push_str(r#","sourceRoot":"#);
507            json_quote_into(&mut json, root);
508        }
509
510        json.push_str(r#","sources":["#);
511        for (i, s) in self.sources.iter().enumerate() {
512            if i > 0 {
513                json.push(',');
514            }
515            json_quote_into(&mut json, s);
516        }
517        json.push(']');
518
519        if !self.sources_content.is_empty() && self.sources_content.iter().any(|c| c.is_some()) {
520            json.push_str(r#","sourcesContent":["#);
521            for (i, c) in self.sources_content.iter().enumerate() {
522                if i > 0 {
523                    json.push(',');
524                }
525                match c {
526                    Some(content) => json_quote_into(&mut json, content),
527                    None => json.push_str("null"),
528                }
529            }
530            json.push(']');
531        }
532
533        json.push_str(r#","names":["#);
534        for (i, n) in self.names.iter().enumerate() {
535            if i > 0 {
536                json.push(',');
537            }
538            json_quote_into(&mut json, n);
539        }
540        json.push(']');
541
542        json.push_str(r#","mappings":"#);
543        json_quote_into(&mut json, &mappings);
544
545        if !self.ignore_list.is_empty() {
546            json.push_str(r#","ignoreList":["#);
547            for (i, &idx) in self.ignore_list.iter().enumerate() {
548                if i > 0 {
549                    json.push(',');
550                }
551                json.push_str(&idx.to_string());
552            }
553            json.push(']');
554        }
555
556        json.push('}');
557        json
558    }
559
560    /// Encode all mappings back to a VLQ mappings string.
561    fn encode_mappings(&self) -> String {
562        if self.mappings.is_empty() {
563            return String::new();
564        }
565
566        let mut out: Vec<u8> = Vec::with_capacity(self.mappings.len() * 6);
567
568        let mut prev_gen_col: i64 = 0;
569        let mut prev_source: i64 = 0;
570        let mut prev_orig_line: i64 = 0;
571        let mut prev_orig_col: i64 = 0;
572        let mut prev_name: i64 = 0;
573        let mut prev_gen_line: u32 = 0;
574        let mut first_in_line = true;
575
576        for m in &self.mappings {
577            while prev_gen_line < m.generated_line {
578                out.push(b';');
579                prev_gen_line += 1;
580                prev_gen_col = 0;
581                first_in_line = true;
582            }
583
584            if !first_in_line {
585                out.push(b',');
586            }
587            first_in_line = false;
588
589            srcmap_codec::vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
590            prev_gen_col = m.generated_column as i64;
591
592            if m.source != NO_SOURCE {
593                srcmap_codec::vlq_encode(&mut out, m.source as i64 - prev_source);
594                prev_source = m.source as i64;
595
596                srcmap_codec::vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
597                prev_orig_line = m.original_line as i64;
598
599                srcmap_codec::vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
600                prev_orig_col = m.original_column as i64;
601
602                if m.name != NO_NAME {
603                    srcmap_codec::vlq_encode(&mut out, m.name as i64 - prev_name);
604                    prev_name = m.name as i64;
605                }
606            }
607        }
608
609        // SAFETY: VLQ output is always valid ASCII
610        unsafe { String::from_utf8_unchecked(out) }
611    }
612}
613
614/// Append a JSON-quoted string to the output buffer.
615fn json_quote_into(out: &mut String, s: &str) {
616    out.push('"');
617    for c in s.chars() {
618        match c {
619            '"' => out.push_str("\\\""),
620            '\\' => out.push_str("\\\\"),
621            '\n' => out.push_str("\\n"),
622            '\r' => out.push_str("\\r"),
623            '\t' => out.push_str("\\t"),
624            c if c < '\x20' => {
625                out.push_str(&format!("\\u{:04x}", c as u32));
626            }
627            c => out.push(c),
628        }
629    }
630    out.push('"');
631}
632
633// ── Internal: decode VLQ mappings directly into flat Mapping vec ───
634
635/// Base64 decode lookup table (byte → 6-bit value, 0xFF = invalid).
636const B64: [u8; 128] = {
637    let mut table = [0xFFu8; 128];
638    let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
639    let mut i = 0u8;
640    while i < 64 {
641        table[chars[i as usize] as usize] = i;
642        i += 1;
643    }
644    table
645};
646
647/// Inline VLQ decode optimized for the hot path (no function call overhead).
648/// Most source map VLQ values fit in 1-2 base64 characters.
649#[inline(always)]
650fn vlq_fast(bytes: &[u8], pos: &mut usize) -> Result<i64, DecodeError> {
651    let p = *pos;
652    if p >= bytes.len() {
653        return Err(DecodeError::UnexpectedEof { offset: p });
654    }
655
656    let b0 = bytes[p];
657    if b0 >= 128 {
658        return Err(DecodeError::InvalidBase64 {
659            byte: b0,
660            offset: p,
661        });
662    }
663    let d0 = B64[b0 as usize];
664    if d0 == 0xFF {
665        return Err(DecodeError::InvalidBase64 {
666            byte: b0,
667            offset: p,
668        });
669    }
670
671    // Fast path: single character VLQ (values -15..15)
672    if (d0 & 0x20) == 0 {
673        *pos = p + 1;
674        let val = (d0 >> 1) as i64;
675        return Ok(if (d0 & 1) != 0 { -val } else { val });
676    }
677
678    // Multi-character VLQ
679    let mut result: u64 = (d0 & 0x1F) as u64;
680    let mut shift: u32 = 5;
681    let mut i = p + 1;
682
683    loop {
684        if i >= bytes.len() {
685            return Err(DecodeError::UnexpectedEof { offset: i });
686        }
687        let b = bytes[i];
688        if b >= 128 {
689            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
690        }
691        let d = B64[b as usize];
692        if d == 0xFF {
693            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
694        }
695        i += 1;
696
697        if shift >= 64 {
698            return Err(DecodeError::VlqOverflow { offset: p });
699        }
700
701        result += ((d & 0x1F) as u64) << shift;
702        shift += 5;
703
704        if (d & 0x20) == 0 {
705            break;
706        }
707    }
708
709    *pos = i;
710    let value = if (result & 1) == 1 {
711        -((result >> 1) as i64)
712    } else {
713        (result >> 1) as i64
714    };
715    Ok(value)
716}
717
718fn decode_mappings(input: &str) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
719    if input.is_empty() {
720        return Ok((Vec::new(), vec![0]));
721    }
722
723    let bytes = input.as_bytes();
724    let len = bytes.len();
725
726    // Pre-count for capacity hints
727    let line_count = bytes.iter().filter(|&&b| b == b';').count() + 1;
728    let approx_segments = bytes.iter().filter(|&&b| b == b',').count() + line_count;
729
730    let mut mappings: Vec<Mapping> = Vec::with_capacity(approx_segments);
731    let mut line_offsets: Vec<u32> = Vec::with_capacity(line_count + 1);
732
733    let mut source_index: i64 = 0;
734    let mut original_line: i64 = 0;
735    let mut original_column: i64 = 0;
736    let mut name_index: i64 = 0;
737    let mut generated_line: u32 = 0;
738    let mut pos: usize = 0;
739
740    loop {
741        line_offsets.push(mappings.len() as u32);
742        let mut generated_column: i64 = 0;
743        let mut saw_semicolon = false;
744
745        while pos < len {
746            let byte = bytes[pos];
747
748            if byte == b';' {
749                pos += 1;
750                saw_semicolon = true;
751                break;
752            }
753
754            if byte == b',' {
755                pos += 1;
756                continue;
757            }
758
759            // Field 1: generated column
760            generated_column += vlq_fast(bytes, &mut pos)?;
761
762            if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
763                // Fields 2-4: source, original line, original column
764                source_index += vlq_fast(bytes, &mut pos)?;
765                original_line += vlq_fast(bytes, &mut pos)?;
766                original_column += vlq_fast(bytes, &mut pos)?;
767
768                // Field 5: name (optional)
769                let name = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
770                    name_index += vlq_fast(bytes, &mut pos)?;
771                    name_index as u32
772                } else {
773                    NO_NAME
774                };
775
776                mappings.push(Mapping {
777                    generated_line,
778                    generated_column: generated_column as u32,
779                    source: source_index as u32,
780                    original_line: original_line as u32,
781                    original_column: original_column as u32,
782                    name,
783                });
784            } else {
785                // 1-field segment: no source info
786                mappings.push(Mapping {
787                    generated_line,
788                    generated_column: generated_column as u32,
789                    source: NO_SOURCE,
790                    original_line: 0,
791                    original_column: 0,
792                    name: NO_NAME,
793                });
794            }
795        }
796
797        if !saw_semicolon {
798            break;
799        }
800        generated_line += 1;
801    }
802
803    // Sentinel for line range computation
804    line_offsets.push(mappings.len() as u32);
805
806    Ok((mappings, line_offsets))
807}
808
809/// Build reverse index: mapping indices sorted by (source, original_line, original_column).
810fn build_reverse_index(mappings: &[Mapping]) -> Vec<u32> {
811    let mut indices: Vec<u32> = (0..mappings.len() as u32)
812        .filter(|&i| mappings[i as usize].source != NO_SOURCE)
813        .collect();
814
815    indices.sort_unstable_by(|&a, &b| {
816        let ma = &mappings[a as usize];
817        let mb = &mappings[b as usize];
818        ma.source
819            .cmp(&mb.source)
820            .then(ma.original_line.cmp(&mb.original_line))
821            .then(ma.original_column.cmp(&mb.original_column))
822    });
823
824    indices
825}
826
827// ── Tests ──────────────────────────────────────────────────────────
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn simple_map() -> &'static str {
834        r#"{"version":3,"sources":["input.js"],"names":["hello"],"mappings":"AAAA;AACA,EAAA;AACA"}"#
835    }
836
837    #[test]
838    fn parse_basic() {
839        let sm = SourceMap::from_json(simple_map()).unwrap();
840        assert_eq!(sm.sources, vec!["input.js"]);
841        assert_eq!(sm.names, vec!["hello"]);
842        assert_eq!(sm.line_count(), 3);
843        assert!(sm.mapping_count() > 0);
844    }
845
846    #[test]
847    fn to_json_roundtrip() {
848        let json = simple_map();
849        let sm = SourceMap::from_json(json).unwrap();
850        let output = sm.to_json();
851
852        // Parse the output back and verify it produces identical lookups
853        let sm2 = SourceMap::from_json(&output).unwrap();
854        assert_eq!(sm2.sources, sm.sources);
855        assert_eq!(sm2.names, sm.names);
856        assert_eq!(sm2.mapping_count(), sm.mapping_count());
857        assert_eq!(sm2.line_count(), sm.line_count());
858
859        // Verify all lookups match
860        for m in sm.all_mappings() {
861            let loc1 = sm.original_position_for(m.generated_line, m.generated_column);
862            let loc2 = sm2.original_position_for(m.generated_line, m.generated_column);
863            match (loc1, loc2) {
864                (Some(a), Some(b)) => {
865                    assert_eq!(a.source, b.source);
866                    assert_eq!(a.line, b.line);
867                    assert_eq!(a.column, b.column);
868                    assert_eq!(a.name, b.name);
869                }
870                (None, None) => {}
871                _ => panic!(
872                    "lookup mismatch at ({}, {})",
873                    m.generated_line, m.generated_column
874                ),
875            }
876        }
877    }
878
879    #[test]
880    fn to_json_roundtrip_large() {
881        let json = generate_test_sourcemap(50, 10, 3);
882        let sm = SourceMap::from_json(&json).unwrap();
883        let output = sm.to_json();
884        let sm2 = SourceMap::from_json(&output).unwrap();
885
886        assert_eq!(sm2.mapping_count(), sm.mapping_count());
887
888        // Spot-check lookups
889        for line in (0..sm.line_count() as u32).step_by(5) {
890            for col in [0u32, 10, 20, 50] {
891                let a = sm.original_position_for(line, col);
892                let b = sm2.original_position_for(line, col);
893                match (a, b) {
894                    (Some(a), Some(b)) => {
895                        assert_eq!(a.source, b.source);
896                        assert_eq!(a.line, b.line);
897                        assert_eq!(a.column, b.column);
898                    }
899                    (None, None) => {}
900                    _ => panic!("mismatch at ({line}, {col})"),
901                }
902            }
903        }
904    }
905
906    #[test]
907    fn to_json_preserves_fields() {
908        let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["app.ts"],"sourcesContent":["const x = 1;"],"names":["x"],"mappings":"AAAAA","ignoreList":[0]}"#;
909        let sm = SourceMap::from_json(json).unwrap();
910        let output = sm.to_json();
911
912        assert!(output.contains(r#""file":"out.js""#));
913        assert!(output.contains(r#""sourceRoot":"src/""#));
914        assert!(output.contains(r#""sourcesContent":["const x = 1;"]"#));
915        assert!(output.contains(r#""ignoreList":[0]"#));
916
917        // Note: sources will have sourceRoot prepended
918        let sm2 = SourceMap::from_json(&output).unwrap();
919        assert_eq!(sm2.file.as_deref(), Some("out.js"));
920        assert_eq!(sm2.ignore_list, vec![0]);
921    }
922
923    #[test]
924    fn original_position_for_exact_match() {
925        let sm = SourceMap::from_json(simple_map()).unwrap();
926        let loc = sm.original_position_for(0, 0).unwrap();
927        assert_eq!(loc.source, 0);
928        assert_eq!(loc.line, 0);
929        assert_eq!(loc.column, 0);
930    }
931
932    #[test]
933    fn original_position_for_column_within_segment() {
934        let sm = SourceMap::from_json(simple_map()).unwrap();
935        // Column 5 on line 1: should snap to the mapping at column 2
936        let loc = sm.original_position_for(1, 5);
937        assert!(loc.is_some());
938    }
939
940    #[test]
941    fn original_position_for_nonexistent_line() {
942        let sm = SourceMap::from_json(simple_map()).unwrap();
943        assert!(sm.original_position_for(999, 0).is_none());
944    }
945
946    #[test]
947    fn original_position_for_before_first_mapping() {
948        // Line 1 first mapping is at column 2. Column 0 should return None.
949        let sm = SourceMap::from_json(simple_map()).unwrap();
950        let loc = sm.original_position_for(1, 0);
951        // Column 0 on line 1: the first mapping at col 0 (AACA decodes to col=0, src delta=1...)
952        // Actually depends on exact VLQ values. Let's just verify it doesn't crash.
953        let _ = loc;
954    }
955
956    #[test]
957    fn generated_position_for_basic() {
958        let sm = SourceMap::from_json(simple_map()).unwrap();
959        let loc = sm.generated_position_for("input.js", 0, 0).unwrap();
960        assert_eq!(loc.line, 0);
961        assert_eq!(loc.column, 0);
962    }
963
964    #[test]
965    fn generated_position_for_unknown_source() {
966        let sm = SourceMap::from_json(simple_map()).unwrap();
967        assert!(sm.generated_position_for("nonexistent.js", 0, 0).is_none());
968    }
969
970    #[test]
971    fn parse_invalid_version() {
972        let json = r#"{"version":2,"sources":[],"names":[],"mappings":""}"#;
973        let err = SourceMap::from_json(json).unwrap_err();
974        assert!(matches!(err, ParseError::InvalidVersion(2)));
975    }
976
977    #[test]
978    fn parse_empty_mappings() {
979        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
980        let sm = SourceMap::from_json(json).unwrap();
981        assert_eq!(sm.mapping_count(), 0);
982        assert!(sm.original_position_for(0, 0).is_none());
983    }
984
985    #[test]
986    fn parse_with_source_root() {
987        let json = r#"{"version":3,"sourceRoot":"src/","sources":["foo.js"],"names":[],"mappings":"AAAA"}"#;
988        let sm = SourceMap::from_json(json).unwrap();
989        assert_eq!(sm.sources, vec!["src/foo.js"]);
990    }
991
992    #[test]
993    fn parse_with_sources_content() {
994        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var x = 1;"],"names":[],"mappings":"AAAA"}"#;
995        let sm = SourceMap::from_json(json).unwrap();
996        assert_eq!(sm.sources_content, vec![Some("var x = 1;".to_string())]);
997    }
998
999    #[test]
1000    fn mappings_for_line() {
1001        let sm = SourceMap::from_json(simple_map()).unwrap();
1002        let line0 = sm.mappings_for_line(0);
1003        assert!(!line0.is_empty());
1004        let empty = sm.mappings_for_line(999);
1005        assert!(empty.is_empty());
1006    }
1007
1008    #[test]
1009    fn large_sourcemap_lookup() {
1010        // Generate a realistic source map
1011        let json = generate_test_sourcemap(500, 20, 5);
1012        let sm = SourceMap::from_json(&json).unwrap();
1013
1014        // Verify lookups work across the whole map
1015        for line in [0, 10, 100, 250, 499] {
1016            let mappings = sm.mappings_for_line(line);
1017            if let Some(m) = mappings.first() {
1018                let loc = sm.original_position_for(line, m.generated_column);
1019                assert!(loc.is_some(), "lookup failed for line {line}");
1020            }
1021        }
1022    }
1023
1024    #[test]
1025    fn reverse_lookup_roundtrip() {
1026        let json = generate_test_sourcemap(100, 10, 3);
1027        let sm = SourceMap::from_json(&json).unwrap();
1028
1029        // Pick a mapping and verify forward + reverse roundtrip
1030        let mapping = &sm.mappings[50];
1031        if mapping.source != NO_SOURCE {
1032            let source_name = sm.source(mapping.source);
1033            let result = sm.generated_position_for(
1034                source_name,
1035                mapping.original_line,
1036                mapping.original_column,
1037            );
1038            assert!(result.is_some(), "reverse lookup failed");
1039        }
1040    }
1041
1042    #[test]
1043    fn indexed_source_map() {
1044        let json = r#"{
1045            "version": 3,
1046            "file": "bundle.js",
1047            "sections": [
1048                {
1049                    "offset": {"line": 0, "column": 0},
1050                    "map": {
1051                        "version": 3,
1052                        "sources": ["a.js"],
1053                        "names": ["foo"],
1054                        "mappings": "AAAAA"
1055                    }
1056                },
1057                {
1058                    "offset": {"line": 10, "column": 0},
1059                    "map": {
1060                        "version": 3,
1061                        "sources": ["b.js"],
1062                        "names": ["bar"],
1063                        "mappings": "AAAAA"
1064                    }
1065                }
1066            ]
1067        }"#;
1068
1069        let sm = SourceMap::from_json(json).unwrap();
1070
1071        // Should have both sources
1072        assert_eq!(sm.sources.len(), 2);
1073        assert!(sm.sources.contains(&"a.js".to_string()));
1074        assert!(sm.sources.contains(&"b.js".to_string()));
1075
1076        // Should have both names
1077        assert_eq!(sm.names.len(), 2);
1078        assert!(sm.names.contains(&"foo".to_string()));
1079        assert!(sm.names.contains(&"bar".to_string()));
1080
1081        // First section: line 0, col 0 should map to a.js
1082        let loc = sm.original_position_for(0, 0).unwrap();
1083        assert_eq!(sm.source(loc.source), "a.js");
1084        assert_eq!(loc.line, 0);
1085        assert_eq!(loc.column, 0);
1086
1087        // Second section: line 10, col 0 should map to b.js
1088        let loc = sm.original_position_for(10, 0).unwrap();
1089        assert_eq!(sm.source(loc.source), "b.js");
1090        assert_eq!(loc.line, 0);
1091        assert_eq!(loc.column, 0);
1092    }
1093
1094    #[test]
1095    fn indexed_source_map_shared_sources() {
1096        // Two sections referencing the same source
1097        let json = r#"{
1098            "version": 3,
1099            "sections": [
1100                {
1101                    "offset": {"line": 0, "column": 0},
1102                    "map": {
1103                        "version": 3,
1104                        "sources": ["shared.js"],
1105                        "names": [],
1106                        "mappings": "AAAA"
1107                    }
1108                },
1109                {
1110                    "offset": {"line": 5, "column": 0},
1111                    "map": {
1112                        "version": 3,
1113                        "sources": ["shared.js"],
1114                        "names": [],
1115                        "mappings": "AACA"
1116                    }
1117                }
1118            ]
1119        }"#;
1120
1121        let sm = SourceMap::from_json(json).unwrap();
1122
1123        // Should deduplicate sources
1124        assert_eq!(sm.sources.len(), 1);
1125        assert_eq!(sm.sources[0], "shared.js");
1126
1127        // Both sections should resolve to the same source
1128        let loc0 = sm.original_position_for(0, 0).unwrap();
1129        let loc5 = sm.original_position_for(5, 0).unwrap();
1130        assert_eq!(loc0.source, loc5.source);
1131    }
1132
1133    #[test]
1134    fn parse_ignore_list() {
1135        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
1136        let sm = SourceMap::from_json(json).unwrap();
1137        assert_eq!(sm.ignore_list, vec![1]);
1138    }
1139
1140    /// Helper: build a source map JSON from absolute mappings data.
1141    fn build_sourcemap_json(
1142        sources: &[&str],
1143        names: &[&str],
1144        mappings_data: &[Vec<Vec<i64>>],
1145    ) -> String {
1146        let mappings_vec: Vec<Vec<Vec<i64>>> = mappings_data.to_vec();
1147        let encoded = srcmap_codec::encode(&mappings_vec);
1148        format!(
1149            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
1150            sources
1151                .iter()
1152                .map(|s| format!("\"{s}\""))
1153                .collect::<Vec<_>>()
1154                .join(","),
1155            names
1156                .iter()
1157                .map(|n| format!("\"{n}\""))
1158                .collect::<Vec<_>>()
1159                .join(","),
1160            encoded,
1161        )
1162    }
1163
1164    // ── 1. Edge cases in decode_mappings ────────────────────────────
1165
1166    #[test]
1167    fn decode_multiple_consecutive_semicolons() {
1168        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
1169        let sm = SourceMap::from_json(json).unwrap();
1170        assert_eq!(sm.line_count(), 4);
1171        assert!(sm.mappings_for_line(1).is_empty());
1172        assert!(sm.mappings_for_line(2).is_empty());
1173        assert!(!sm.mappings_for_line(0).is_empty());
1174        assert!(!sm.mappings_for_line(3).is_empty());
1175    }
1176
1177    #[test]
1178    fn decode_trailing_semicolons() {
1179        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;"}"#;
1180        let sm = SourceMap::from_json(json).unwrap();
1181        assert_eq!(sm.line_count(), 3);
1182        assert!(!sm.mappings_for_line(0).is_empty());
1183        assert!(sm.mappings_for_line(1).is_empty());
1184        assert!(sm.mappings_for_line(2).is_empty());
1185    }
1186
1187    #[test]
1188    fn decode_leading_comma() {
1189        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":",AAAA"}"#;
1190        let sm = SourceMap::from_json(json).unwrap();
1191        assert_eq!(sm.mapping_count(), 1);
1192        let m = &sm.all_mappings()[0];
1193        assert_eq!(m.generated_line, 0);
1194        assert_eq!(m.generated_column, 0);
1195    }
1196
1197    #[test]
1198    fn decode_single_field_segments() {
1199        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,C"}"#;
1200        let sm = SourceMap::from_json(json).unwrap();
1201        assert_eq!(sm.mapping_count(), 2);
1202        for m in sm.all_mappings() {
1203            assert_eq!(m.source, NO_SOURCE);
1204        }
1205        assert_eq!(sm.all_mappings()[0].generated_column, 0);
1206        assert_eq!(sm.all_mappings()[1].generated_column, 1);
1207        assert!(sm.original_position_for(0, 0).is_none());
1208        assert!(sm.original_position_for(0, 1).is_none());
1209    }
1210
1211    #[test]
1212    fn decode_five_field_segments_with_names() {
1213        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0, 0], vec![10, 0, 0, 5, 1]]];
1214        let json = build_sourcemap_json(&["app.js"], &["foo", "bar"], &mappings_data);
1215        let sm = SourceMap::from_json(&json).unwrap();
1216        assert_eq!(sm.mapping_count(), 2);
1217        assert_eq!(sm.all_mappings()[0].name, 0);
1218        assert_eq!(sm.all_mappings()[1].name, 1);
1219
1220        let loc = sm.original_position_for(0, 0).unwrap();
1221        assert_eq!(loc.name, Some(0));
1222        assert_eq!(sm.name(0), "foo");
1223
1224        let loc = sm.original_position_for(0, 10).unwrap();
1225        assert_eq!(loc.name, Some(1));
1226        assert_eq!(sm.name(1), "bar");
1227    }
1228
1229    #[test]
1230    fn decode_large_vlq_values() {
1231        let mappings_data = vec![vec![vec![500_i64, 0, 1000, 2000]]];
1232        let json = build_sourcemap_json(&["big.js"], &[], &mappings_data);
1233        let sm = SourceMap::from_json(&json).unwrap();
1234        assert_eq!(sm.mapping_count(), 1);
1235        let m = &sm.all_mappings()[0];
1236        assert_eq!(m.generated_column, 500);
1237        assert_eq!(m.original_line, 1000);
1238        assert_eq!(m.original_column, 2000);
1239
1240        let loc = sm.original_position_for(0, 500).unwrap();
1241        assert_eq!(loc.line, 1000);
1242        assert_eq!(loc.column, 2000);
1243    }
1244
1245    #[test]
1246    fn decode_only_semicolons() {
1247        let json = r#"{"version":3,"sources":[],"names":[],"mappings":";;;"}"#;
1248        let sm = SourceMap::from_json(json).unwrap();
1249        assert_eq!(sm.line_count(), 4);
1250        assert_eq!(sm.mapping_count(), 0);
1251        for line in 0..4 {
1252            assert!(sm.mappings_for_line(line).is_empty());
1253        }
1254    }
1255
1256    #[test]
1257    fn decode_mixed_single_and_four_field_segments() {
1258        let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
1259        let four_field_encoded = srcmap_codec::encode(&mappings_data);
1260        let combined_mappings = format!("A,{four_field_encoded}");
1261        let json = format!(
1262            r#"{{"version":3,"sources":["x.js"],"names":[],"mappings":"{combined_mappings}"}}"#,
1263        );
1264        let sm = SourceMap::from_json(&json).unwrap();
1265        assert_eq!(sm.mapping_count(), 2);
1266        assert_eq!(sm.all_mappings()[0].source, NO_SOURCE);
1267        assert_eq!(sm.all_mappings()[1].source, 0);
1268    }
1269
1270    // ── 2. Source map parsing ───────────────────────────────────────
1271
1272    #[test]
1273    fn parse_missing_optional_fields() {
1274        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1275        let sm = SourceMap::from_json(json).unwrap();
1276        assert!(sm.file.is_none());
1277        assert!(sm.source_root.is_none());
1278        assert!(sm.sources_content.is_empty());
1279        assert!(sm.ignore_list.is_empty());
1280    }
1281
1282    #[test]
1283    fn parse_with_file_field() {
1284        let json =
1285            r#"{"version":3,"file":"output.js","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1286        let sm = SourceMap::from_json(json).unwrap();
1287        assert_eq!(sm.file.as_deref(), Some("output.js"));
1288    }
1289
1290    #[test]
1291    fn parse_null_entries_in_sources() {
1292        let json = r#"{"version":3,"sources":["a.js",null,"c.js"],"names":[],"mappings":"AAAA"}"#;
1293        let sm = SourceMap::from_json(json).unwrap();
1294        assert_eq!(sm.sources.len(), 3);
1295        assert_eq!(sm.sources[0], "a.js");
1296        assert_eq!(sm.sources[1], "");
1297        assert_eq!(sm.sources[2], "c.js");
1298    }
1299
1300    #[test]
1301    fn parse_null_entries_in_sources_with_source_root() {
1302        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js",null],"names":[],"mappings":"AAAA"}"#;
1303        let sm = SourceMap::from_json(json).unwrap();
1304        assert_eq!(sm.sources[0], "lib/a.js");
1305        assert_eq!(sm.sources[1], "");
1306    }
1307
1308    #[test]
1309    fn parse_empty_names_array() {
1310        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1311        let sm = SourceMap::from_json(json).unwrap();
1312        assert!(sm.names.is_empty());
1313    }
1314
1315    #[test]
1316    fn parse_invalid_json() {
1317        let result = SourceMap::from_json("not valid json");
1318        assert!(result.is_err());
1319        assert!(matches!(result.unwrap_err(), ParseError::Json(_)));
1320    }
1321
1322    #[test]
1323    fn parse_json_missing_version() {
1324        let result = SourceMap::from_json(r#"{"sources":[],"names":[],"mappings":""}"#);
1325        assert!(result.is_err());
1326    }
1327
1328    #[test]
1329    fn parse_multiple_sources_overlapping_original_positions() {
1330        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10], vec![10, 1, 5, 10]]];
1331        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1332        let sm = SourceMap::from_json(&json).unwrap();
1333
1334        let loc0 = sm.original_position_for(0, 0).unwrap();
1335        assert_eq!(loc0.source, 0);
1336        assert_eq!(sm.source(loc0.source), "a.js");
1337
1338        let loc1 = sm.original_position_for(0, 10).unwrap();
1339        assert_eq!(loc1.source, 1);
1340        assert_eq!(sm.source(loc1.source), "b.js");
1341
1342        assert_eq!(loc0.line, loc1.line);
1343        assert_eq!(loc0.column, loc1.column);
1344    }
1345
1346    #[test]
1347    fn parse_sources_content_with_null_entries() {
1348        let json = r#"{"version":3,"sources":["a.js","b.js"],"sourcesContent":["content a",null],"names":[],"mappings":"AAAA"}"#;
1349        let sm = SourceMap::from_json(json).unwrap();
1350        assert_eq!(sm.sources_content.len(), 2);
1351        assert_eq!(sm.sources_content[0], Some("content a".to_string()));
1352        assert_eq!(sm.sources_content[1], None);
1353    }
1354
1355    #[test]
1356    fn parse_empty_sources_and_names() {
1357        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1358        let sm = SourceMap::from_json(json).unwrap();
1359        assert!(sm.sources.is_empty());
1360        assert!(sm.names.is_empty());
1361        assert_eq!(sm.mapping_count(), 0);
1362    }
1363
1364    // ── 3. Position lookups ─────────────────────────────────────────
1365
1366    #[test]
1367    fn lookup_exact_match() {
1368        let mappings_data = vec![vec![
1369            vec![0_i64, 0, 10, 20],
1370            vec![5, 0, 10, 25],
1371            vec![15, 0, 11, 0],
1372        ]];
1373        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1374        let sm = SourceMap::from_json(&json).unwrap();
1375
1376        let loc = sm.original_position_for(0, 5).unwrap();
1377        assert_eq!(loc.line, 10);
1378        assert_eq!(loc.column, 25);
1379    }
1380
1381    #[test]
1382    fn lookup_before_first_segment() {
1383        let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
1384        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1385        let sm = SourceMap::from_json(&json).unwrap();
1386
1387        assert!(sm.original_position_for(0, 0).is_none());
1388        assert!(sm.original_position_for(0, 4).is_none());
1389    }
1390
1391    #[test]
1392    fn lookup_between_segments() {
1393        let mappings_data = vec![vec![
1394            vec![0_i64, 0, 1, 0],
1395            vec![10, 0, 2, 0],
1396            vec![20, 0, 3, 0],
1397        ]];
1398        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1399        let sm = SourceMap::from_json(&json).unwrap();
1400
1401        let loc = sm.original_position_for(0, 7).unwrap();
1402        assert_eq!(loc.line, 1);
1403        assert_eq!(loc.column, 0);
1404
1405        let loc = sm.original_position_for(0, 15).unwrap();
1406        assert_eq!(loc.line, 2);
1407        assert_eq!(loc.column, 0);
1408    }
1409
1410    #[test]
1411    fn lookup_after_last_segment() {
1412        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 1, 5]]];
1413        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1414        let sm = SourceMap::from_json(&json).unwrap();
1415
1416        let loc = sm.original_position_for(0, 100).unwrap();
1417        assert_eq!(loc.line, 1);
1418        assert_eq!(loc.column, 5);
1419    }
1420
1421    #[test]
1422    fn lookup_empty_lines_no_mappings() {
1423        let mappings_data = vec![
1424            vec![vec![0_i64, 0, 0, 0]],
1425            vec![],
1426            vec![vec![0_i64, 0, 2, 0]],
1427        ];
1428        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1429        let sm = SourceMap::from_json(&json).unwrap();
1430
1431        assert!(sm.original_position_for(1, 0).is_none());
1432        assert!(sm.original_position_for(1, 10).is_none());
1433        assert!(sm.original_position_for(0, 0).is_some());
1434        assert!(sm.original_position_for(2, 0).is_some());
1435    }
1436
1437    #[test]
1438    fn lookup_line_with_single_mapping() {
1439        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1440        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1441        let sm = SourceMap::from_json(&json).unwrap();
1442
1443        let loc = sm.original_position_for(0, 0).unwrap();
1444        assert_eq!(loc.line, 0);
1445        assert_eq!(loc.column, 0);
1446
1447        let loc = sm.original_position_for(0, 50).unwrap();
1448        assert_eq!(loc.line, 0);
1449        assert_eq!(loc.column, 0);
1450    }
1451
1452    #[test]
1453    fn lookup_column_0_vs_column_nonzero() {
1454        let mappings_data = vec![vec![vec![0_i64, 0, 10, 0], vec![8, 0, 20, 5]]];
1455        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1456        let sm = SourceMap::from_json(&json).unwrap();
1457
1458        let loc0 = sm.original_position_for(0, 0).unwrap();
1459        assert_eq!(loc0.line, 10);
1460        assert_eq!(loc0.column, 0);
1461
1462        let loc8 = sm.original_position_for(0, 8).unwrap();
1463        assert_eq!(loc8.line, 20);
1464        assert_eq!(loc8.column, 5);
1465
1466        let loc4 = sm.original_position_for(0, 4).unwrap();
1467        assert_eq!(loc4.line, 10);
1468    }
1469
1470    #[test]
1471    fn lookup_beyond_last_line() {
1472        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1473        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1474        let sm = SourceMap::from_json(&json).unwrap();
1475
1476        assert!(sm.original_position_for(1, 0).is_none());
1477        assert!(sm.original_position_for(100, 0).is_none());
1478    }
1479
1480    #[test]
1481    fn lookup_single_field_returns_none() {
1482        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A"}"#;
1483        let sm = SourceMap::from_json(json).unwrap();
1484        assert_eq!(sm.mapping_count(), 1);
1485        assert!(sm.original_position_for(0, 0).is_none());
1486    }
1487
1488    // ── 4. Reverse lookups (generated_position_for) ─────────────────
1489
1490    #[test]
1491    fn reverse_lookup_exact_match() {
1492        let mappings_data = vec![
1493            vec![vec![0_i64, 0, 0, 0]],
1494            vec![vec![4, 0, 1, 0], vec![10, 0, 1, 8]],
1495            vec![vec![0, 0, 2, 0]],
1496        ];
1497        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1498        let sm = SourceMap::from_json(&json).unwrap();
1499
1500        let loc = sm.generated_position_for("main.js", 1, 8).unwrap();
1501        assert_eq!(loc.line, 1);
1502        assert_eq!(loc.column, 10);
1503    }
1504
1505    #[test]
1506    fn reverse_lookup_no_match() {
1507        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10]]];
1508        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1509        let sm = SourceMap::from_json(&json).unwrap();
1510
1511        assert!(sm.generated_position_for("main.js", 99, 0).is_none());
1512    }
1513
1514    #[test]
1515    fn reverse_lookup_unknown_source() {
1516        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1517        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1518        let sm = SourceMap::from_json(&json).unwrap();
1519
1520        assert!(sm.generated_position_for("unknown.js", 0, 0).is_none());
1521    }
1522
1523    #[test]
1524    fn reverse_lookup_multiple_mappings_same_original() {
1525        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]], vec![vec![20, 0, 5, 10]]];
1526        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1527        let sm = SourceMap::from_json(&json).unwrap();
1528
1529        let loc = sm.generated_position_for("src.js", 5, 10);
1530        assert!(loc.is_some());
1531        let loc = loc.unwrap();
1532        assert!(
1533            (loc.line == 0 && loc.column == 0) || (loc.line == 1 && loc.column == 20),
1534            "Expected (0,0) or (1,20), got ({},{})",
1535            loc.line,
1536            loc.column
1537        );
1538    }
1539
1540    #[test]
1541    fn reverse_lookup_with_multiple_sources() {
1542        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 1, 0, 0]]];
1543        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1544        let sm = SourceMap::from_json(&json).unwrap();
1545
1546        let loc_a = sm.generated_position_for("a.js", 0, 0).unwrap();
1547        assert_eq!(loc_a.line, 0);
1548        assert_eq!(loc_a.column, 0);
1549
1550        let loc_b = sm.generated_position_for("b.js", 0, 0).unwrap();
1551        assert_eq!(loc_b.line, 0);
1552        assert_eq!(loc_b.column, 10);
1553    }
1554
1555    #[test]
1556    fn reverse_lookup_skips_single_field_segments() {
1557        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
1558        let sm = SourceMap::from_json(&json).unwrap();
1559
1560        let loc = sm.generated_position_for("a.js", 0, 0).unwrap();
1561        assert_eq!(loc.line, 0);
1562        assert_eq!(loc.column, 5);
1563    }
1564
1565    #[test]
1566    fn reverse_lookup_finds_each_original_line() {
1567        let mappings_data = vec![
1568            vec![vec![0_i64, 0, 0, 0]],
1569            vec![vec![0, 0, 1, 0]],
1570            vec![vec![0, 0, 2, 0]],
1571            vec![vec![0, 0, 3, 0]],
1572        ];
1573        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1574        let sm = SourceMap::from_json(&json).unwrap();
1575
1576        for orig_line in 0..4 {
1577            let loc = sm.generated_position_for("x.js", orig_line, 0).unwrap();
1578            assert_eq!(
1579                loc.line, orig_line,
1580                "reverse lookup for orig line {orig_line}"
1581            );
1582            assert_eq!(loc.column, 0);
1583        }
1584    }
1585
1586    // ── 5. ignoreList ───────────────────────────────────────────────
1587
1588    #[test]
1589    fn parse_with_ignore_list_multiple() {
1590        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js","vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[1,2]}"#;
1591        let sm = SourceMap::from_json(json).unwrap();
1592        assert_eq!(sm.ignore_list, vec![1, 2]);
1593    }
1594
1595    #[test]
1596    fn parse_with_empty_ignore_list() {
1597        let json =
1598            r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA","ignoreList":[]}"#;
1599        let sm = SourceMap::from_json(json).unwrap();
1600        assert!(sm.ignore_list.is_empty());
1601    }
1602
1603    #[test]
1604    fn parse_without_ignore_list_field() {
1605        let json = r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA"}"#;
1606        let sm = SourceMap::from_json(json).unwrap();
1607        assert!(sm.ignore_list.is_empty());
1608    }
1609
1610    // ── Additional edge case tests ──────────────────────────────────
1611
1612    #[test]
1613    fn source_index_lookup() {
1614        let json = r#"{"version":3,"sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA"}"#;
1615        let sm = SourceMap::from_json(json).unwrap();
1616        assert_eq!(sm.source_index("a.js"), Some(0));
1617        assert_eq!(sm.source_index("b.js"), Some(1));
1618        assert_eq!(sm.source_index("c.js"), Some(2));
1619        assert_eq!(sm.source_index("d.js"), None);
1620    }
1621
1622    #[test]
1623    fn all_mappings_returns_complete_list() {
1624        let mappings_data = vec![
1625            vec![vec![0_i64, 0, 0, 0], vec![5, 0, 0, 5]],
1626            vec![vec![0, 0, 1, 0]],
1627        ];
1628        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1629        let sm = SourceMap::from_json(&json).unwrap();
1630        assert_eq!(sm.all_mappings().len(), 3);
1631        assert_eq!(sm.mapping_count(), 3);
1632    }
1633
1634    #[test]
1635    fn line_count_matches_decoded_lines() {
1636        let mappings_data = vec![
1637            vec![vec![0_i64, 0, 0, 0]],
1638            vec![],
1639            vec![vec![0_i64, 0, 2, 0]],
1640            vec![],
1641            vec![],
1642        ];
1643        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1644        let sm = SourceMap::from_json(&json).unwrap();
1645        assert_eq!(sm.line_count(), 5);
1646    }
1647
1648    #[test]
1649    fn parse_error_display() {
1650        let err = ParseError::InvalidVersion(5);
1651        assert_eq!(format!("{err}"), "unsupported source map version: 5");
1652
1653        let json_err = SourceMap::from_json("{}").unwrap_err();
1654        let display = format!("{json_err}");
1655        assert!(display.contains("JSON parse error") || display.contains("missing field"));
1656    }
1657
1658    #[test]
1659    fn original_position_name_none_for_four_field() {
1660        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]]];
1661        let json = build_sourcemap_json(&["a.js"], &["unused_name"], &mappings_data);
1662        let sm = SourceMap::from_json(&json).unwrap();
1663
1664        let loc = sm.original_position_for(0, 0).unwrap();
1665        assert!(loc.name.is_none());
1666    }
1667
1668    #[test]
1669    fn forward_and_reverse_roundtrip_comprehensive() {
1670        let mappings_data = vec![
1671            vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10], vec![20, 1, 5, 0]],
1672            vec![vec![0, 0, 1, 0], vec![5, 1, 6, 3]],
1673            vec![vec![0, 0, 2, 0]],
1674        ];
1675        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1676        let sm = SourceMap::from_json(&json).unwrap();
1677
1678        for m in sm.all_mappings() {
1679            if m.source == NO_SOURCE {
1680                continue;
1681            }
1682            let source_name = sm.source(m.source);
1683
1684            let orig = sm
1685                .original_position_for(m.generated_line, m.generated_column)
1686                .unwrap();
1687            assert_eq!(orig.source, m.source);
1688            assert_eq!(orig.line, m.original_line);
1689            assert_eq!(orig.column, m.original_column);
1690
1691            let gen_loc = sm
1692                .generated_position_for(source_name, m.original_line, m.original_column)
1693                .unwrap();
1694            assert_eq!(gen_loc.line, m.generated_line);
1695            assert_eq!(gen_loc.column, m.generated_column);
1696        }
1697    }
1698
1699    // ── 6. Comprehensive edge case tests ────────────────────────────
1700
1701    // -- sourceRoot edge cases --
1702
1703    #[test]
1704    fn source_root_with_multiple_sources() {
1705        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA,KACA,KACA"}"#;
1706        let sm = SourceMap::from_json(json).unwrap();
1707        assert_eq!(sm.sources, vec!["lib/a.js", "lib/b.js", "lib/c.js"]);
1708    }
1709
1710    #[test]
1711    fn source_root_empty_string() {
1712        let json = r#"{"version":3,"sourceRoot":"","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1713        let sm = SourceMap::from_json(json).unwrap();
1714        assert_eq!(sm.sources, vec!["a.js"]);
1715    }
1716
1717    #[test]
1718    fn source_root_preserved_in_to_json() {
1719        let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1720        let sm = SourceMap::from_json(json).unwrap();
1721        let output = sm.to_json();
1722        assert!(output.contains(r#""sourceRoot":"src/""#));
1723    }
1724
1725    #[test]
1726    fn source_root_reverse_lookup_uses_prefixed_name() {
1727        let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1728        let sm = SourceMap::from_json(json).unwrap();
1729        // Must use the prefixed name for reverse lookups
1730        assert!(sm.generated_position_for("src/a.js", 0, 0).is_some());
1731        assert!(sm.generated_position_for("a.js", 0, 0).is_none());
1732    }
1733
1734    #[test]
1735    fn source_root_with_trailing_slash() {
1736        let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1737        let sm = SourceMap::from_json(json).unwrap();
1738        assert_eq!(sm.sources[0], "src/a.js");
1739    }
1740
1741    #[test]
1742    fn source_root_without_trailing_slash() {
1743        let json = r#"{"version":3,"sourceRoot":"src","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1744        let sm = SourceMap::from_json(json).unwrap();
1745        assert_eq!(sm.sources[0], "srca.js"); // No auto-slash — sourceRoot is raw prefix
1746    }
1747
1748    // -- JSON/parsing error cases --
1749
1750    #[test]
1751    fn parse_empty_json_object() {
1752        // {} has no version field
1753        let result = SourceMap::from_json("{}");
1754        assert!(result.is_err());
1755    }
1756
1757    #[test]
1758    fn parse_version_0() {
1759        let json = r#"{"version":0,"sources":[],"names":[],"mappings":""}"#;
1760        assert!(matches!(
1761            SourceMap::from_json(json).unwrap_err(),
1762            ParseError::InvalidVersion(0)
1763        ));
1764    }
1765
1766    #[test]
1767    fn parse_version_4() {
1768        let json = r#"{"version":4,"sources":[],"names":[],"mappings":""}"#;
1769        assert!(matches!(
1770            SourceMap::from_json(json).unwrap_err(),
1771            ParseError::InvalidVersion(4)
1772        ));
1773    }
1774
1775    #[test]
1776    fn parse_extra_unknown_fields_ignored() {
1777        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom_field":true,"x_debug":{"foo":"bar"}}"#;
1778        let sm = SourceMap::from_json(json).unwrap();
1779        assert_eq!(sm.mapping_count(), 1);
1780    }
1781
1782    #[test]
1783    fn parse_vlq_error_propagated() {
1784        // '!' is not valid base64 — should surface as VLQ error
1785        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AA!A"}"#;
1786        let result = SourceMap::from_json(json);
1787        assert!(result.is_err());
1788        assert!(matches!(result.unwrap_err(), ParseError::Vlq(_)));
1789    }
1790
1791    #[test]
1792    fn parse_truncated_vlq_error() {
1793        // 'g' has continuation bit set — truncated VLQ
1794        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"g"}"#;
1795        let result = SourceMap::from_json(json);
1796        assert!(result.is_err());
1797    }
1798
1799    // -- to_json edge cases --
1800
1801    #[test]
1802    fn to_json_produces_valid_json() {
1803        let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["a.ts","b.ts"],"sourcesContent":["const x = 1;\nconst y = \"hello\";",null],"names":["x","y"],"mappings":"AAAAA,KACAC;AACA","ignoreList":[1]}"#;
1804        let sm = SourceMap::from_json(json).unwrap();
1805        let output = sm.to_json();
1806        // Must be valid JSON that serde can parse
1807        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
1808    }
1809
1810    #[test]
1811    fn to_json_escapes_special_chars() {
1812        let json = r#"{"version":3,"sources":["path/with\"quotes.js"],"sourcesContent":["line1\nline2\ttab\\backslash"],"names":[],"mappings":"AAAA"}"#;
1813        let sm = SourceMap::from_json(json).unwrap();
1814        let output = sm.to_json();
1815        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
1816        let sm2 = SourceMap::from_json(&output).unwrap();
1817        assert_eq!(sm2.sources_content[0].as_deref(), Some("line1\nline2\ttab\\backslash"));
1818    }
1819
1820    #[test]
1821    fn to_json_empty_map() {
1822        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1823        let sm = SourceMap::from_json(json).unwrap();
1824        let output = sm.to_json();
1825        let sm2 = SourceMap::from_json(&output).unwrap();
1826        assert_eq!(sm2.mapping_count(), 0);
1827        assert!(sm2.sources.is_empty());
1828    }
1829
1830    #[test]
1831    fn to_json_roundtrip_with_names() {
1832        let mappings_data = vec![vec![
1833            vec![0_i64, 0, 0, 0, 0],
1834            vec![10, 0, 0, 10, 1],
1835            vec![20, 0, 1, 0, 2],
1836        ]];
1837        let json = build_sourcemap_json(&["src.js"], &["foo", "bar", "baz"], &mappings_data);
1838        let sm = SourceMap::from_json(&json).unwrap();
1839        let output = sm.to_json();
1840        let sm2 = SourceMap::from_json(&output).unwrap();
1841
1842        for m in sm2.all_mappings() {
1843            if m.source != NO_SOURCE && m.name != NO_NAME {
1844                let loc = sm2.original_position_for(m.generated_line, m.generated_column).unwrap();
1845                assert!(loc.name.is_some());
1846            }
1847        }
1848    }
1849
1850    // -- Indexed source map edge cases --
1851
1852    #[test]
1853    fn indexed_source_map_column_offset() {
1854        let json = r#"{
1855            "version": 3,
1856            "sections": [
1857                {
1858                    "offset": {"line": 0, "column": 10},
1859                    "map": {
1860                        "version": 3,
1861                        "sources": ["a.js"],
1862                        "names": [],
1863                        "mappings": "AAAA"
1864                    }
1865                }
1866            ]
1867        }"#;
1868        let sm = SourceMap::from_json(json).unwrap();
1869        // Mapping at col 0 in section should be offset to col 10 (first line only)
1870        let loc = sm.original_position_for(0, 10).unwrap();
1871        assert_eq!(loc.line, 0);
1872        assert_eq!(loc.column, 0);
1873        // Before the offset should have no mapping
1874        assert!(sm.original_position_for(0, 0).is_none());
1875    }
1876
1877    #[test]
1878    fn indexed_source_map_column_offset_only_first_line() {
1879        // Column offset only applies to the first line of a section
1880        let json = r#"{
1881            "version": 3,
1882            "sections": [
1883                {
1884                    "offset": {"line": 0, "column": 20},
1885                    "map": {
1886                        "version": 3,
1887                        "sources": ["a.js"],
1888                        "names": [],
1889                        "mappings": "AAAA;AAAA"
1890                    }
1891                }
1892            ]
1893        }"#;
1894        let sm = SourceMap::from_json(json).unwrap();
1895        // Line 0: column offset applies
1896        let loc = sm.original_position_for(0, 20).unwrap();
1897        assert_eq!(loc.column, 0);
1898        // Line 1: column offset does NOT apply
1899        let loc = sm.original_position_for(1, 0).unwrap();
1900        assert_eq!(loc.column, 0);
1901    }
1902
1903    #[test]
1904    fn indexed_source_map_empty_section() {
1905        let json = r#"{
1906            "version": 3,
1907            "sections": [
1908                {
1909                    "offset": {"line": 0, "column": 0},
1910                    "map": {
1911                        "version": 3,
1912                        "sources": [],
1913                        "names": [],
1914                        "mappings": ""
1915                    }
1916                },
1917                {
1918                    "offset": {"line": 5, "column": 0},
1919                    "map": {
1920                        "version": 3,
1921                        "sources": ["b.js"],
1922                        "names": [],
1923                        "mappings": "AAAA"
1924                    }
1925                }
1926            ]
1927        }"#;
1928        let sm = SourceMap::from_json(json).unwrap();
1929        assert_eq!(sm.sources.len(), 1);
1930        let loc = sm.original_position_for(5, 0).unwrap();
1931        assert_eq!(sm.source(loc.source), "b.js");
1932    }
1933
1934    #[test]
1935    fn indexed_source_map_with_sources_content() {
1936        let json = r#"{
1937            "version": 3,
1938            "sections": [
1939                {
1940                    "offset": {"line": 0, "column": 0},
1941                    "map": {
1942                        "version": 3,
1943                        "sources": ["a.js"],
1944                        "sourcesContent": ["var a = 1;"],
1945                        "names": [],
1946                        "mappings": "AAAA"
1947                    }
1948                },
1949                {
1950                    "offset": {"line": 5, "column": 0},
1951                    "map": {
1952                        "version": 3,
1953                        "sources": ["b.js"],
1954                        "sourcesContent": ["var b = 2;"],
1955                        "names": [],
1956                        "mappings": "AAAA"
1957                    }
1958                }
1959            ]
1960        }"#;
1961        let sm = SourceMap::from_json(json).unwrap();
1962        assert_eq!(sm.sources_content.len(), 2);
1963        assert_eq!(sm.sources_content[0], Some("var a = 1;".to_string()));
1964        assert_eq!(sm.sources_content[1], Some("var b = 2;".to_string()));
1965    }
1966
1967    #[test]
1968    fn indexed_source_map_with_ignore_list() {
1969        let json = r#"{
1970            "version": 3,
1971            "sections": [
1972                {
1973                    "offset": {"line": 0, "column": 0},
1974                    "map": {
1975                        "version": 3,
1976                        "sources": ["app.js", "vendor.js"],
1977                        "names": [],
1978                        "mappings": "AAAA",
1979                        "ignoreList": [1]
1980                    }
1981                }
1982            ]
1983        }"#;
1984        let sm = SourceMap::from_json(json).unwrap();
1985        assert!(!sm.ignore_list.is_empty());
1986    }
1987
1988    // -- Boundary conditions --
1989
1990    #[test]
1991    fn lookup_max_column_on_line() {
1992        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1993        let json = build_sourcemap_json(&["a.js"], &[], &mappings_data);
1994        let sm = SourceMap::from_json(&json).unwrap();
1995        // Very large column — should snap to the last mapping on line
1996        let loc = sm.original_position_for(0, u32::MAX - 1).unwrap();
1997        assert_eq!(loc.line, 0);
1998        assert_eq!(loc.column, 0);
1999    }
2000
2001    #[test]
2002    fn mappings_for_line_beyond_end() {
2003        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2004        let sm = SourceMap::from_json(json).unwrap();
2005        assert!(sm.mappings_for_line(u32::MAX).is_empty());
2006    }
2007
2008    #[test]
2009    fn source_with_unicode_path() {
2010        let json = r#"{"version":3,"sources":["src/日本語.ts"],"names":["変数"],"mappings":"AAAAA"}"#;
2011        let sm = SourceMap::from_json(json).unwrap();
2012        assert_eq!(sm.sources[0], "src/日本語.ts");
2013        assert_eq!(sm.names[0], "変数");
2014        let loc = sm.original_position_for(0, 0).unwrap();
2015        assert_eq!(sm.source(loc.source), "src/日本語.ts");
2016        assert_eq!(sm.name(loc.name.unwrap()), "変数");
2017    }
2018
2019    #[test]
2020    fn to_json_roundtrip_unicode_sources() {
2021        let json = r#"{"version":3,"sources":["src/日本語.ts"],"sourcesContent":["const 変数 = 1;"],"names":["変数"],"mappings":"AAAAA"}"#;
2022        let sm = SourceMap::from_json(json).unwrap();
2023        let output = sm.to_json();
2024        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
2025        let sm2 = SourceMap::from_json(&output).unwrap();
2026        assert_eq!(sm2.sources[0], "src/日本語.ts");
2027        assert_eq!(sm2.sources_content[0], Some("const 変数 = 1;".to_string()));
2028    }
2029
2030    #[test]
2031    fn many_sources_lookup() {
2032        // 100 sources, verify source_index works for all
2033        let sources: Vec<String> = (0..100).map(|i| format!("src/file{i}.js")).collect();
2034        let source_strs: Vec<&str> = sources.iter().map(|s| s.as_str()).collect();
2035        let mappings_data = vec![sources.iter().enumerate().map(|(i, _)| {
2036            vec![(i * 10) as i64, i as i64, 0, 0]
2037        }).collect::<Vec<_>>()];
2038        let json = build_sourcemap_json(&source_strs, &[], &mappings_data);
2039        let sm = SourceMap::from_json(&json).unwrap();
2040
2041        for (i, src) in sources.iter().enumerate() {
2042            assert_eq!(sm.source_index(src), Some(i as u32));
2043        }
2044    }
2045
2046    #[test]
2047    fn clone_sourcemap() {
2048        let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA"}"#;
2049        let sm = SourceMap::from_json(json).unwrap();
2050        let sm2 = sm.clone();
2051        assert_eq!(sm2.sources, sm.sources);
2052        assert_eq!(sm2.mapping_count(), sm.mapping_count());
2053        let loc = sm2.original_position_for(0, 0).unwrap();
2054        assert_eq!(sm2.source(loc.source), "a.js");
2055    }
2056
2057    /// Generate a test source map JSON with realistic structure.
2058    fn generate_test_sourcemap(lines: usize, segs_per_line: usize, num_sources: usize) -> String {
2059        let sources: Vec<String> = (0..num_sources)
2060            .map(|i| format!("src/file{i}.js"))
2061            .collect();
2062        let names: Vec<String> = (0..20).map(|i| format!("var{i}")).collect();
2063
2064        let mut mappings_parts = Vec::with_capacity(lines);
2065        let mut gen_col;
2066        let mut src: i64 = 0;
2067        let mut src_line: i64 = 0;
2068        let mut src_col: i64 = 0;
2069        let mut name: i64 = 0;
2070
2071        for _ in 0..lines {
2072            gen_col = 0i64;
2073            let mut line_parts = Vec::with_capacity(segs_per_line);
2074
2075            for s in 0..segs_per_line {
2076                let gc_delta = 2 + (s as i64 * 3) % 20;
2077                gen_col += gc_delta;
2078
2079                let src_delta = if s % 7 == 0 { 1 } else { 0 };
2080                src = (src + src_delta) % num_sources as i64;
2081
2082                src_line += 1;
2083                let sc_delta = (s as i64 * 5 + 1) % 30;
2084                src_col = sc_delta;
2085
2086                let has_name = s % 4 == 0;
2087                if has_name {
2088                    name = (name + 1) % names.len() as i64;
2089                }
2090
2091                // Build segment using codec encode
2092                let segment = if has_name {
2093                    vec![gen_col, src, src_line, src_col, name]
2094                } else {
2095                    vec![gen_col, src, src_line, src_col]
2096                };
2097
2098                line_parts.push(segment);
2099            }
2100
2101            mappings_parts.push(line_parts);
2102        }
2103
2104        let encoded = srcmap_codec::encode(&mappings_parts);
2105
2106        format!(
2107            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
2108            sources
2109                .iter()
2110                .map(|s| format!("\"{s}\""))
2111                .collect::<Vec<_>>()
2112                .join(","),
2113            names
2114                .iter()
2115                .map(|n| format!("\"{n}\""))
2116                .collect::<Vec<_>>()
2117                .join(","),
2118            encoded,
2119        )
2120    }
2121}