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, RefCell};
27use std::collections::HashMap;
28use std::fmt;
29
30use serde::Deserialize;
31use srcmap_codec::{DecodeError, vlq_encode_unsigned};
32use srcmap_scopes::ScopeInfo;
33
34// ── Constants ──────────────────────────────────────────────────────
35
36const NO_SOURCE: u32 = u32::MAX;
37const NO_NAME: u32 = u32::MAX;
38
39// ── Public types ───────────────────────────────────────────────────
40
41/// A single decoded mapping entry. Compact at 28 bytes (6 × u32 + bool with padding).
42///
43/// Maps a position in the generated output to an optional position in an
44/// original source file. Stored contiguously in a `Vec<Mapping>` sorted by
45/// `(generated_line, generated_column)` for cache-friendly binary search.
46#[derive(Debug, Clone, Copy)]
47pub struct Mapping {
48    /// 0-based line in the generated output.
49    pub generated_line: u32,
50    /// 0-based column in the generated output.
51    pub generated_column: u32,
52    /// Index into `SourceMap::sources`. `u32::MAX` if this mapping has no source.
53    pub source: u32,
54    /// 0-based line in the original source (only meaningful when `source != u32::MAX`).
55    pub original_line: u32,
56    /// 0-based column in the original source (only meaningful when `source != u32::MAX`).
57    pub original_column: u32,
58    /// Index into `SourceMap::names`. `u32::MAX` if this mapping has no name.
59    pub name: u32,
60    /// Whether this mapping is a range mapping (ECMA-426).
61    pub is_range_mapping: bool,
62}
63
64/// Result of an [`SourceMap::original_position_for`] lookup.
65///
66/// All indices are 0-based. Use [`SourceMap::source`] and [`SourceMap::name`]
67/// to resolve the `source` and `name` indices to strings.
68#[derive(Debug, Clone)]
69pub struct OriginalLocation {
70    /// Index into `SourceMap::sources`.
71    pub source: u32,
72    /// 0-based line in the original source.
73    pub line: u32,
74    /// 0-based column in the original source.
75    pub column: u32,
76    /// Index into `SourceMap::names`, if the mapping has a name.
77    pub name: Option<u32>,
78}
79
80/// Result of a [`SourceMap::generated_position_for`] lookup.
81///
82/// All values are 0-based.
83#[derive(Debug, Clone)]
84pub struct GeneratedLocation {
85    /// 0-based line in the generated output.
86    pub line: u32,
87    /// 0-based column in the generated output.
88    pub column: u32,
89}
90
91/// Search bias for position lookups.
92///
93/// Controls how non-exact matches are resolved during binary search:
94/// - `GreatestLowerBound` (default): find the closest mapping at or before the position
95/// - `LeastUpperBound`: find the closest mapping at or after the position
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
97pub enum Bias {
98    /// Return the closest position at or before the requested position (default).
99    #[default]
100    GreatestLowerBound,
101    /// Return the closest position at or after the requested position.
102    LeastUpperBound,
103}
104
105/// A mapped range: original start/end positions for a generated range.
106///
107/// Returned by [`SourceMap::map_range`]. Both endpoints must resolve to the
108/// same source file.
109#[derive(Debug, Clone)]
110pub struct MappedRange {
111    /// Index into `SourceMap::sources`.
112    pub source: u32,
113    /// 0-based start line in the original source.
114    pub original_start_line: u32,
115    /// 0-based start column in the original source.
116    pub original_start_column: u32,
117    /// 0-based end line in the original source.
118    pub original_end_line: u32,
119    /// 0-based end column in the original source.
120    pub original_end_column: u32,
121}
122
123/// Errors that can occur during source map parsing.
124#[derive(Debug)]
125pub enum ParseError {
126    /// The JSON could not be deserialized.
127    Json(serde_json::Error),
128    /// The VLQ mappings string is malformed.
129    Vlq(DecodeError),
130    /// The `version` field is not `3`.
131    InvalidVersion(u32),
132    /// The ECMA-426 scopes data could not be decoded.
133    Scopes(srcmap_scopes::ScopesError),
134    /// A section map in an indexed source map is itself an indexed map (not allowed per ECMA-426).
135    NestedIndexMap,
136    /// Sections in an indexed source map are not in ascending (line, column) order.
137    SectionsNotOrdered,
138}
139
140impl fmt::Display for ParseError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            Self::Json(e) => write!(f, "JSON parse error: {e}"),
144            Self::Vlq(e) => write!(f, "VLQ decode error: {e}"),
145            Self::InvalidVersion(v) => write!(f, "unsupported source map version: {v}"),
146            Self::Scopes(e) => write!(f, "scopes decode error: {e}"),
147            Self::NestedIndexMap => write!(f, "section map must not be an indexed source map"),
148            Self::SectionsNotOrdered => {
149                write!(f, "sections must be in ascending (line, column) order")
150            }
151        }
152    }
153}
154
155impl std::error::Error for ParseError {
156    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157        match self {
158            Self::Json(e) => Some(e),
159            Self::Vlq(e) => Some(e),
160            Self::Scopes(e) => Some(e),
161            Self::InvalidVersion(_) | Self::NestedIndexMap | Self::SectionsNotOrdered => None,
162        }
163    }
164}
165
166impl From<serde_json::Error> for ParseError {
167    fn from(e: serde_json::Error) -> Self {
168        Self::Json(e)
169    }
170}
171
172impl From<DecodeError> for ParseError {
173    fn from(e: DecodeError) -> Self {
174        Self::Vlq(e)
175    }
176}
177
178impl From<srcmap_scopes::ScopesError> for ParseError {
179    fn from(e: srcmap_scopes::ScopesError) -> Self {
180        Self::Scopes(e)
181    }
182}
183
184// ── Helpers ────────────────────────────────────────────────────────
185
186/// Resolve source filenames by applying `source_root` prefix and replacing `None` with empty string.
187fn resolve_sources(raw_sources: &[Option<String>], source_root: &str) -> Vec<String> {
188    raw_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
198/// Build a source filename -> index lookup map.
199fn build_source_map(sources: &[String]) -> HashMap<String, u32> {
200    sources
201        .iter()
202        .enumerate()
203        .map(|(i, s)| (s.clone(), i as u32))
204        .collect()
205}
206
207// ── Raw JSON structure ─────────────────────────────────────────────
208
209#[derive(Deserialize)]
210struct RawSourceMap<'a> {
211    version: u32,
212    #[serde(default)]
213    file: Option<String>,
214    #[serde(default, rename = "sourceRoot")]
215    source_root: Option<String>,
216    #[serde(default)]
217    sources: Vec<Option<String>>,
218    #[serde(default, rename = "sourcesContent")]
219    sources_content: Option<Vec<Option<String>>>,
220    #[serde(default)]
221    names: Vec<String>,
222    #[serde(default, borrow)]
223    mappings: &'a str,
224    #[serde(default, rename = "ignoreList")]
225    ignore_list: Option<Vec<u32>>,
226    /// Deprecated Chrome DevTools field, fallback for `ignoreList`.
227    #[serde(default, rename = "x_google_ignoreList")]
228    x_google_ignore_list: Option<Vec<u32>>,
229    /// Debug ID for associating generated files with source maps (ECMA-426).
230    /// Accepts both `debugId` (spec) and `debug_id` (Sentry compat).
231    #[serde(default, rename = "debugId", alias = "debug_id")]
232    debug_id: Option<String>,
233    /// Scopes and variables (ECMA-426 scopes proposal).
234    #[serde(default, borrow)]
235    scopes: Option<&'a str>,
236    /// Range mappings (ECMA-426).
237    #[serde(default, borrow, rename = "rangeMappings")]
238    range_mappings: Option<&'a str>,
239    /// Indexed source maps use `sections` instead of `mappings`.
240    #[serde(default)]
241    sections: Option<Vec<RawSection>>,
242    /// Catch-all for unknown extension fields (x_*).
243    #[serde(flatten)]
244    extensions: HashMap<String, serde_json::Value>,
245}
246
247/// A section in an indexed source map.
248#[derive(Deserialize)]
249struct RawSection {
250    offset: RawOffset,
251    map: Box<serde_json::value::RawValue>,
252}
253
254#[derive(Deserialize)]
255struct RawOffset {
256    line: u32,
257    column: u32,
258}
259
260// ── SourceMap ──────────────────────────────────────────────────────
261
262/// A fully-parsed source map with O(log n) position lookups.
263///
264/// Supports both regular and indexed (sectioned) source maps, `ignoreList`,
265/// `debugId`, scopes (ECMA-426), and extension fields. All positions are
266/// 0-based lines and columns.
267///
268/// # Construction
269///
270/// - [`SourceMap::from_json`] — parse from a JSON string (most common)
271/// - [`SourceMap::from_parts`] — build from pre-decoded components
272/// - [`SourceMap::from_vlq`] — parse from pre-extracted parts + raw VLQ string
273/// - [`SourceMap::from_json_lines`] — partial parse for a line range
274///
275/// # Lookups
276///
277/// - [`SourceMap::original_position_for`] — forward: generated → original
278/// - [`SourceMap::generated_position_for`] — reverse: original → generated (lazy index)
279/// - [`SourceMap::all_generated_positions_for`] — all reverse matches
280/// - [`SourceMap::map_range`] — map a generated range to its original range
281///
282/// For cases where you only need a few lookups and want to avoid decoding
283/// all mappings upfront, see [`LazySourceMap`].
284#[derive(Debug, Clone)]
285pub struct SourceMap {
286    pub file: Option<String>,
287    pub source_root: Option<String>,
288    pub sources: Vec<String>,
289    pub sources_content: Vec<Option<String>>,
290    pub names: Vec<String>,
291    pub ignore_list: Vec<u32>,
292    /// Extension fields (x_* keys) preserved for passthrough.
293    pub extensions: HashMap<String, serde_json::Value>,
294    /// Debug ID (UUID) for associating generated files with source maps (ECMA-426).
295    pub debug_id: Option<String>,
296    /// Decoded scope and variable information (ECMA-426 scopes proposal).
297    pub scopes: Option<ScopeInfo>,
298
299    /// Flat decoded mappings, ordered by (generated_line, generated_column).
300    mappings: Vec<Mapping>,
301
302    /// `line_offsets[i]` = index of first mapping on generated line `i`.
303    /// `line_offsets[line_count]` = mappings.len() (sentinel).
304    line_offsets: Vec<u32>,
305
306    /// Indices into `mappings`, sorted by (source, original_line, original_column).
307    /// Built lazily on first `generated_position_for` call.
308    reverse_index: OnceCell<Vec<u32>>,
309
310    /// Source filename → index for O(1) lookup by name.
311    source_map: HashMap<String, u32>,
312
313    /// Cached flag: true if any mapping has `is_range_mapping == true`.
314    has_range_mappings: bool,
315}
316
317impl SourceMap {
318    /// Parse a source map from a JSON string.
319    /// Supports both regular and indexed (sectioned) source maps.
320    pub fn from_json(json: &str) -> Result<Self, ParseError> {
321        Self::from_json_inner(json, true)
322    }
323
324    /// Internal parser with control over whether indexed maps (sections) are allowed.
325    fn from_json_inner(json: &str, allow_sections: bool) -> Result<Self, ParseError> {
326        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
327
328        if raw.version != 3 {
329            return Err(ParseError::InvalidVersion(raw.version));
330        }
331
332        // Handle indexed source maps (sections)
333        if let Some(sections) = raw.sections {
334            if !allow_sections {
335                return Err(ParseError::NestedIndexMap);
336            }
337            return Self::from_sections(raw.file, sections);
338        }
339
340        Self::from_regular(raw)
341    }
342
343    /// Parse a regular (non-indexed) source map.
344    fn from_regular(raw: RawSourceMap<'_>) -> Result<Self, ParseError> {
345        let source_root = raw.source_root.as_deref().unwrap_or("");
346        let sources = resolve_sources(&raw.sources, source_root);
347        let sources_content = raw.sources_content.unwrap_or_default();
348        let source_map = build_source_map(&sources);
349
350        // Decode mappings directly into flat Mapping vec
351        let (mut mappings, line_offsets) = decode_mappings(raw.mappings)?;
352
353        // Decode range mappings if present
354        if let Some(range_mappings_str) = raw.range_mappings
355            && !range_mappings_str.is_empty()
356        {
357            decode_range_mappings(range_mappings_str, &mut mappings, &line_offsets)?;
358        }
359
360        // Decode scopes if present
361        let num_sources = sources.len();
362        let scopes = match raw.scopes {
363            Some(scopes_str) if !scopes_str.is_empty() => Some(srcmap_scopes::decode_scopes(
364                scopes_str,
365                &raw.names,
366                num_sources,
367            )?),
368            _ => None,
369        };
370
371        // Use x_google_ignoreList as fallback only when ignoreList is absent
372        let ignore_list = match raw.ignore_list {
373            Some(list) => list,
374            None => raw.x_google_ignore_list.unwrap_or_default(),
375        };
376
377        // Filter extensions to only keep x_* and x-* fields
378        let extensions: HashMap<String, serde_json::Value> = raw
379            .extensions
380            .into_iter()
381            .filter(|(k, _)| k.starts_with("x_") || k.starts_with("x-"))
382            .collect();
383
384        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
385
386        Ok(Self {
387            file: raw.file,
388            source_root: raw.source_root,
389            sources,
390            sources_content,
391            names: raw.names,
392            ignore_list,
393            extensions,
394            debug_id: raw.debug_id,
395            scopes,
396            mappings,
397            line_offsets,
398            reverse_index: OnceCell::new(),
399            source_map,
400            has_range_mappings,
401        })
402    }
403
404    /// Flatten an indexed source map (with sections) into a regular one.
405    fn from_sections(file: Option<String>, sections: Vec<RawSection>) -> Result<Self, ParseError> {
406        let mut all_sources: Vec<String> = Vec::new();
407        let mut all_sources_content: Vec<Option<String>> = Vec::new();
408        let mut all_names: Vec<String> = Vec::new();
409        let mut all_mappings: Vec<Mapping> = Vec::new();
410        let mut all_ignore_list: Vec<u32> = Vec::new();
411        let mut max_line: u32 = 0;
412
413        // Source/name dedup maps to merge across sections
414        let mut source_index_map: HashMap<String, u32> = HashMap::new();
415        let mut name_index_map: HashMap<String, u32> = HashMap::new();
416
417        // Validate section ordering (must be in ascending line, column order)
418        for i in 1..sections.len() {
419            let prev = &sections[i - 1].offset;
420            let curr = &sections[i].offset;
421            if (curr.line, curr.column) <= (prev.line, prev.column) {
422                return Err(ParseError::SectionsNotOrdered);
423            }
424        }
425
426        for section in &sections {
427            // Section maps must not be indexed maps themselves (ECMA-426)
428            let sub = Self::from_json_inner(section.map.get(), false)?;
429
430            let line_offset = section.offset.line;
431            let col_offset = section.offset.column;
432
433            // Map section source indices to global indices
434            let source_remap: Vec<u32> = sub
435                .sources
436                .iter()
437                .enumerate()
438                .map(|(i, s)| {
439                    if let Some(&existing) = source_index_map.get(s) {
440                        existing
441                    } else {
442                        let idx = all_sources.len() as u32;
443                        all_sources.push(s.clone());
444                        // Add sourcesContent if available
445                        let content = sub.sources_content.get(i).cloned().unwrap_or(None);
446                        all_sources_content.push(content);
447                        source_index_map.insert(s.clone(), idx);
448                        idx
449                    }
450                })
451                .collect();
452
453            // Map section name indices to global indices
454            let name_remap: Vec<u32> = sub
455                .names
456                .iter()
457                .map(|n| {
458                    if let Some(&existing) = name_index_map.get(n) {
459                        existing
460                    } else {
461                        let idx = all_names.len() as u32;
462                        all_names.push(n.clone());
463                        name_index_map.insert(n.clone(), idx);
464                        idx
465                    }
466                })
467                .collect();
468
469            // Add ignore_list entries (remapped to global source indices)
470            for &idx in &sub.ignore_list {
471                let global_idx = source_remap[idx as usize];
472                if !all_ignore_list.contains(&global_idx) {
473                    all_ignore_list.push(global_idx);
474                }
475            }
476
477            // Remap and offset all mappings from this section
478            for m in &sub.mappings {
479                let gen_line = m.generated_line + line_offset;
480                let gen_col = if m.generated_line == 0 {
481                    m.generated_column + col_offset
482                } else {
483                    m.generated_column
484                };
485
486                all_mappings.push(Mapping {
487                    generated_line: gen_line,
488                    generated_column: gen_col,
489                    source: if m.source == NO_SOURCE {
490                        NO_SOURCE
491                    } else {
492                        source_remap[m.source as usize]
493                    },
494                    original_line: m.original_line,
495                    original_column: m.original_column,
496                    name: if m.name == NO_NAME {
497                        NO_NAME
498                    } else {
499                        name_remap[m.name as usize]
500                    },
501                    is_range_mapping: m.is_range_mapping,
502                });
503
504                if gen_line > max_line {
505                    max_line = gen_line;
506                }
507            }
508        }
509
510        // Sort mappings by (generated_line, generated_column)
511        all_mappings.sort_unstable_by(|a, b| {
512            a.generated_line
513                .cmp(&b.generated_line)
514                .then(a.generated_column.cmp(&b.generated_column))
515        });
516
517        // Build line_offsets
518        let line_count = if all_mappings.is_empty() {
519            0
520        } else {
521            max_line as usize + 1
522        };
523        let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
524        let mut current_line: usize = 0;
525        for (i, m) in all_mappings.iter().enumerate() {
526            while current_line < m.generated_line as usize {
527                current_line += 1;
528                if current_line < line_offsets.len() {
529                    line_offsets[current_line] = i as u32;
530                }
531            }
532        }
533        // Fill sentinel
534        if !line_offsets.is_empty() {
535            let last = all_mappings.len() as u32;
536            for offset in line_offsets.iter_mut().skip(current_line + 1) {
537                *offset = last;
538            }
539        }
540
541        let source_map = build_source_map(&all_sources);
542        let has_range_mappings = all_mappings.iter().any(|m| m.is_range_mapping);
543
544        Ok(Self {
545            file,
546            source_root: None,
547            sources: all_sources,
548            sources_content: all_sources_content,
549            names: all_names,
550            ignore_list: all_ignore_list,
551            extensions: HashMap::new(),
552            debug_id: None,
553            scopes: None, // TODO: merge scopes from sections
554            mappings: all_mappings,
555            line_offsets,
556            reverse_index: OnceCell::new(),
557            source_map,
558            has_range_mappings,
559        })
560    }
561
562    /// Look up the original source position for a generated position.
563    ///
564    /// Both `line` and `column` are 0-based.
565    /// Returns `None` if no mapping exists or the mapping has no source.
566    pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
567        self.original_position_for_with_bias(line, column, Bias::GreatestLowerBound)
568    }
569
570    /// Look up the original source position with a search bias.
571    ///
572    /// Both `line` and `column` are 0-based.
573    /// - `GreatestLowerBound`: find the closest mapping at or before the column (default)
574    /// - `LeastUpperBound`: find the closest mapping at or after the column
575    pub fn original_position_for_with_bias(
576        &self,
577        line: u32,
578        column: u32,
579        bias: Bias,
580    ) -> Option<OriginalLocation> {
581        let line_idx = line as usize;
582        if line_idx + 1 >= self.line_offsets.len() {
583            return self.range_mapping_fallback(line, column);
584        }
585
586        let start = self.line_offsets[line_idx] as usize;
587        let end = self.line_offsets[line_idx + 1] as usize;
588
589        if start == end {
590            return self.range_mapping_fallback(line, column);
591        }
592
593        let line_mappings = &self.mappings[start..end];
594
595        let idx = match bias {
596            Bias::GreatestLowerBound => {
597                match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
598                    Ok(i) => i,
599                    Err(0) => return self.range_mapping_fallback(line, column),
600                    Err(i) => i - 1,
601                }
602            }
603            Bias::LeastUpperBound => {
604                match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
605                    Ok(i) => i,
606                    Err(i) => {
607                        if i >= line_mappings.len() {
608                            return None;
609                        }
610                        i
611                    }
612                }
613            }
614        };
615
616        let mapping = &line_mappings[idx];
617
618        if mapping.source == NO_SOURCE {
619            return None;
620        }
621
622        if mapping.is_range_mapping && column >= mapping.generated_column {
623            let column_delta = column - mapping.generated_column;
624            return Some(OriginalLocation {
625                source: mapping.source,
626                line: mapping.original_line,
627                column: mapping.original_column + column_delta,
628                name: if mapping.name == NO_NAME {
629                    None
630                } else {
631                    Some(mapping.name)
632                },
633            });
634        }
635
636        Some(OriginalLocation {
637            source: mapping.source,
638            line: mapping.original_line,
639            column: mapping.original_column,
640            name: if mapping.name == NO_NAME {
641                None
642            } else {
643                Some(mapping.name)
644            },
645        })
646    }
647
648    fn range_mapping_fallback(&self, line: u32, column: u32) -> Option<OriginalLocation> {
649        let line_idx = line as usize;
650        let search_end = if line_idx + 1 < self.line_offsets.len() {
651            self.line_offsets[line_idx] as usize
652        } else {
653            self.mappings.len()
654        };
655        if search_end == 0 {
656            return None;
657        }
658        let last_mapping = &self.mappings[search_end - 1];
659        if !last_mapping.is_range_mapping || last_mapping.source == NO_SOURCE {
660            return None;
661        }
662        let line_delta = line - last_mapping.generated_line;
663        let column_delta = if line_delta == 0 {
664            column - last_mapping.generated_column
665        } else {
666            0
667        };
668        Some(OriginalLocation {
669            source: last_mapping.source,
670            line: last_mapping.original_line + line_delta,
671            column: last_mapping.original_column + column_delta,
672            name: if last_mapping.name == NO_NAME {
673                None
674            } else {
675                Some(last_mapping.name)
676            },
677        })
678    }
679
680    /// Look up the generated position for an original source position.
681    ///
682    /// `source` is the source filename. `line` and `column` are 0-based.
683    /// Uses `LeastUpperBound` by default (finds first mapping at or after the position).
684    pub fn generated_position_for(
685        &self,
686        source: &str,
687        line: u32,
688        column: u32,
689    ) -> Option<GeneratedLocation> {
690        self.generated_position_for_with_bias(source, line, column, Bias::LeastUpperBound)
691    }
692
693    /// Look up the generated position with a search bias.
694    ///
695    /// `source` is the source filename. `line` and `column` are 0-based.
696    /// - `GreatestLowerBound`: find the closest mapping at or before the position (default)
697    /// - `LeastUpperBound`: find the closest mapping at or after the position
698    pub fn generated_position_for_with_bias(
699        &self,
700        source: &str,
701        line: u32,
702        column: u32,
703        bias: Bias,
704    ) -> Option<GeneratedLocation> {
705        let &source_idx = self.source_map.get(source)?;
706
707        let reverse_index = self
708            .reverse_index
709            .get_or_init(|| build_reverse_index(&self.mappings));
710
711        // Binary search in reverse_index for (source, line, column)
712        let idx = reverse_index.partition_point(|&i| {
713            let m = &self.mappings[i as usize];
714            (m.source, m.original_line, m.original_column) < (source_idx, line, column)
715        });
716
717        match bias {
718            Bias::GreatestLowerBound => {
719                // partition_point gives us the first element >= target.
720                // For GLB, we want the element at or before.
721                // If exact match at idx, use it. Otherwise use idx-1.
722                if idx < reverse_index.len() {
723                    let mapping = &self.mappings[reverse_index[idx] as usize];
724                    if mapping.source == source_idx
725                        && mapping.original_line == line
726                        && mapping.original_column == column
727                    {
728                        return Some(GeneratedLocation {
729                            line: mapping.generated_line,
730                            column: mapping.generated_column,
731                        });
732                    }
733                }
734                // No exact match: use the element before (greatest lower bound)
735                if idx == 0 {
736                    return None;
737                }
738                let mapping = &self.mappings[reverse_index[idx - 1] as usize];
739                if mapping.source != source_idx {
740                    return None;
741                }
742                Some(GeneratedLocation {
743                    line: mapping.generated_line,
744                    column: mapping.generated_column,
745                })
746            }
747            Bias::LeastUpperBound => {
748                if idx >= reverse_index.len() {
749                    return None;
750                }
751                let mapping = &self.mappings[reverse_index[idx] as usize];
752                if mapping.source != source_idx {
753                    return None;
754                }
755                Some(GeneratedLocation {
756                    line: mapping.generated_line,
757                    column: mapping.generated_column,
758                })
759            }
760        }
761    }
762
763    /// Find all generated positions for an original source position.
764    ///
765    /// `source` is the source filename. `line` and `column` are 0-based.
766    /// Returns all generated positions that map back to this original location.
767    pub fn all_generated_positions_for(
768        &self,
769        source: &str,
770        line: u32,
771        column: u32,
772    ) -> Vec<GeneratedLocation> {
773        let Some(&source_idx) = self.source_map.get(source) else {
774            return Vec::new();
775        };
776
777        let reverse_index = self
778            .reverse_index
779            .get_or_init(|| build_reverse_index(&self.mappings));
780
781        // Find the first entry matching (source, line, column)
782        let start = reverse_index.partition_point(|&i| {
783            let m = &self.mappings[i as usize];
784            (m.source, m.original_line, m.original_column) < (source_idx, line, column)
785        });
786
787        let mut results = Vec::new();
788
789        for &ri in &reverse_index[start..] {
790            let m = &self.mappings[ri as usize];
791            if m.source != source_idx || m.original_line != line || m.original_column != column {
792                break;
793            }
794            results.push(GeneratedLocation {
795                line: m.generated_line,
796                column: m.generated_column,
797            });
798        }
799
800        results
801    }
802
803    /// Map a generated range to its original range.
804    ///
805    /// Given a generated range `(start_line:start_column → end_line:end_column)`,
806    /// maps both endpoints through the source map and returns the original range.
807    /// Both endpoints must resolve to the same source file.
808    pub fn map_range(
809        &self,
810        start_line: u32,
811        start_column: u32,
812        end_line: u32,
813        end_column: u32,
814    ) -> Option<MappedRange> {
815        let start = self.original_position_for(start_line, start_column)?;
816        let end = self.original_position_for(end_line, end_column)?;
817
818        // Both endpoints must map to the same source
819        if start.source != end.source {
820            return None;
821        }
822
823        Some(MappedRange {
824            source: start.source,
825            original_start_line: start.line,
826            original_start_column: start.column,
827            original_end_line: end.line,
828            original_end_column: end.column,
829        })
830    }
831
832    /// Resolve a source index to its filename.
833    ///
834    /// # Panics
835    ///
836    /// Panics if `index` is out of bounds. Use [`get_source`](Self::get_source)
837    /// for a non-panicking alternative.
838    #[inline]
839    pub fn source(&self, index: u32) -> &str {
840        &self.sources[index as usize]
841    }
842
843    /// Resolve a source index to its filename, returning `None` if out of bounds.
844    #[inline]
845    pub fn get_source(&self, index: u32) -> Option<&str> {
846        self.sources.get(index as usize).map(|s| s.as_str())
847    }
848
849    /// Resolve a name index to its string.
850    ///
851    /// # Panics
852    ///
853    /// Panics if `index` is out of bounds. Use [`get_name`](Self::get_name)
854    /// for a non-panicking alternative.
855    #[inline]
856    pub fn name(&self, index: u32) -> &str {
857        &self.names[index as usize]
858    }
859
860    /// Resolve a name index to its string, returning `None` if out of bounds.
861    #[inline]
862    pub fn get_name(&self, index: u32) -> Option<&str> {
863        self.names.get(index as usize).map(|s| s.as_str())
864    }
865
866    /// Find the source index for a filename.
867    #[inline]
868    pub fn source_index(&self, name: &str) -> Option<u32> {
869        self.source_map.get(name).copied()
870    }
871
872    /// Total number of decoded mappings.
873    #[inline]
874    pub fn mapping_count(&self) -> usize {
875        self.mappings.len()
876    }
877
878    /// Number of generated lines.
879    #[inline]
880    pub fn line_count(&self) -> usize {
881        self.line_offsets.len().saturating_sub(1)
882    }
883
884    /// Get all mappings for a generated line (0-based).
885    #[inline]
886    pub fn mappings_for_line(&self, line: u32) -> &[Mapping] {
887        let line_idx = line as usize;
888        if line_idx + 1 >= self.line_offsets.len() {
889            return &[];
890        }
891        let start = self.line_offsets[line_idx] as usize;
892        let end = self.line_offsets[line_idx + 1] as usize;
893        &self.mappings[start..end]
894    }
895
896    /// Iterate all mappings.
897    #[inline]
898    pub fn all_mappings(&self) -> &[Mapping] {
899        &self.mappings
900    }
901
902    /// Serialize the source map back to JSON.
903    ///
904    /// Produces a valid source map v3 JSON string that can be written to a file
905    /// or embedded in a data URL.
906    pub fn to_json(&self) -> String {
907        self.to_json_with_options(false)
908    }
909
910    /// Serialize the source map back to JSON with options.
911    ///
912    /// If `exclude_content` is true, `sourcesContent` is omitted from the output.
913    pub fn to_json_with_options(&self, exclude_content: bool) -> String {
914        let mappings = self.encode_mappings();
915
916        // Encode scopes first — this may add new names that need to be in the names array
917        let scopes_encoded = if let Some(ref scopes_info) = self.scopes {
918            let mut names_clone = self.names.clone();
919            let s = srcmap_scopes::encode_scopes(scopes_info, &mut names_clone);
920            Some((s, names_clone))
921        } else {
922            None
923        };
924        let names_for_json = match &scopes_encoded {
925            Some((_, expanded_names)) => expanded_names,
926            None => &self.names,
927        };
928
929        let source_root_prefix = self.source_root.as_deref().unwrap_or("");
930
931        let mut json = String::with_capacity(256 + mappings.len());
932        json.push_str(r#"{"version":3"#);
933
934        if let Some(ref file) = self.file {
935            json.push_str(r#","file":"#);
936            json_quote_into(&mut json, file);
937        }
938
939        if let Some(ref root) = self.source_root {
940            json.push_str(r#","sourceRoot":"#);
941            json_quote_into(&mut json, root);
942        }
943
944        // Strip sourceRoot prefix from sources to avoid double-application on roundtrip
945        json.push_str(r#","sources":["#);
946        for (i, s) in self.sources.iter().enumerate() {
947            if i > 0 {
948                json.push(',');
949            }
950            let source_name = if !source_root_prefix.is_empty() {
951                s.strip_prefix(source_root_prefix).unwrap_or(s)
952            } else {
953                s
954            };
955            json_quote_into(&mut json, source_name);
956        }
957        json.push(']');
958
959        if !exclude_content
960            && !self.sources_content.is_empty()
961            && self.sources_content.iter().any(|c| c.is_some())
962        {
963            json.push_str(r#","sourcesContent":["#);
964            for (i, c) in self.sources_content.iter().enumerate() {
965                if i > 0 {
966                    json.push(',');
967                }
968                match c {
969                    Some(content) => json_quote_into(&mut json, content),
970                    None => json.push_str("null"),
971                }
972            }
973            json.push(']');
974        }
975
976        json.push_str(r#","names":["#);
977        for (i, n) in names_for_json.iter().enumerate() {
978            if i > 0 {
979                json.push(',');
980            }
981            json_quote_into(&mut json, n);
982        }
983        json.push(']');
984
985        // VLQ mappings are pure base64/,/; — no escaping needed
986        json.push_str(r#","mappings":""#);
987        json.push_str(&mappings);
988        json.push('"');
989
990        if let Some(range_mappings) = self.encode_range_mappings() {
991            // Range mappings are also pure VLQ — no escaping needed
992            json.push_str(r#","rangeMappings":""#);
993            json.push_str(&range_mappings);
994            json.push('"');
995        }
996
997        if !self.ignore_list.is_empty() {
998            use std::fmt::Write;
999            json.push_str(r#","ignoreList":["#);
1000            for (i, &idx) in self.ignore_list.iter().enumerate() {
1001                if i > 0 {
1002                    json.push(',');
1003                }
1004                let _ = write!(json, "{idx}");
1005            }
1006            json.push(']');
1007        }
1008
1009        if let Some(ref id) = self.debug_id {
1010            json.push_str(r#","debugId":"#);
1011            json_quote_into(&mut json, id);
1012        }
1013
1014        // scopes (ECMA-426 scopes proposal)
1015        if let Some((ref s, _)) = scopes_encoded {
1016            json.push_str(r#","scopes":"#);
1017            json_quote_into(&mut json, s);
1018        }
1019
1020        // Emit extension fields (x_* and x-* keys)
1021        let mut ext_keys: Vec<&String> = self.extensions.keys().collect();
1022        ext_keys.sort();
1023        for key in ext_keys {
1024            if let Some(val) = self.extensions.get(key) {
1025                json.push(',');
1026                json_quote_into(&mut json, key);
1027                json.push(':');
1028                json.push_str(&serde_json::to_string(val).unwrap_or_default());
1029            }
1030        }
1031
1032        json.push('}');
1033        json
1034    }
1035
1036    /// Construct a `SourceMap` from pre-built parts.
1037    ///
1038    /// This avoids the encode-then-decode round-trip used in composition pipelines.
1039    /// Mappings must be sorted by (generated_line, generated_column).
1040    /// Use `u32::MAX` for `source`/`name` fields to indicate absence.
1041    #[allow(clippy::too_many_arguments)]
1042    pub fn from_parts(
1043        file: Option<String>,
1044        source_root: Option<String>,
1045        sources: Vec<String>,
1046        sources_content: Vec<Option<String>>,
1047        names: Vec<String>,
1048        mappings: Vec<Mapping>,
1049        ignore_list: Vec<u32>,
1050        debug_id: Option<String>,
1051        scopes: Option<ScopeInfo>,
1052    ) -> Self {
1053        // Build line_offsets from sorted mappings
1054        let line_count = mappings.last().map_or(0, |m| m.generated_line as usize + 1);
1055        let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
1056        let mut current_line: usize = 0;
1057        for (i, m) in mappings.iter().enumerate() {
1058            while current_line < m.generated_line as usize {
1059                current_line += 1;
1060                if current_line < line_offsets.len() {
1061                    line_offsets[current_line] = i as u32;
1062                }
1063            }
1064        }
1065        // Fill remaining with sentinel
1066        if !line_offsets.is_empty() {
1067            let last = mappings.len() as u32;
1068            for offset in line_offsets.iter_mut().skip(current_line + 1) {
1069                *offset = last;
1070            }
1071        }
1072
1073        let source_map = build_source_map(&sources);
1074        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1075
1076        Self {
1077            file,
1078            source_root,
1079            sources,
1080            sources_content,
1081            names,
1082            ignore_list,
1083            extensions: HashMap::new(),
1084            debug_id,
1085            scopes,
1086            mappings,
1087            line_offsets,
1088            reverse_index: OnceCell::new(),
1089            source_map,
1090            has_range_mappings,
1091        }
1092    }
1093
1094    /// Build a source map from pre-parsed components and a VLQ mappings string.
1095    ///
1096    /// This is the fast path for WASM: JS does `JSON.parse()` (V8-native speed),
1097    /// then only the VLQ mappings string crosses into WASM for decoding.
1098    /// Avoids copying large `sourcesContent` into WASM linear memory.
1099    #[allow(clippy::too_many_arguments)]
1100    pub fn from_vlq(
1101        mappings_str: &str,
1102        sources: Vec<String>,
1103        names: Vec<String>,
1104        file: Option<String>,
1105        source_root: Option<String>,
1106        sources_content: Vec<Option<String>>,
1107        ignore_list: Vec<u32>,
1108        debug_id: Option<String>,
1109    ) -> Result<Self, ParseError> {
1110        Self::from_vlq_with_range_mappings(
1111            mappings_str,
1112            sources,
1113            names,
1114            file,
1115            source_root,
1116            sources_content,
1117            ignore_list,
1118            debug_id,
1119            None,
1120        )
1121    }
1122
1123    /// Build a source map from pre-parsed components, a VLQ mappings string,
1124    /// and an optional range mappings string.
1125    #[allow(clippy::too_many_arguments)]
1126    pub fn from_vlq_with_range_mappings(
1127        mappings_str: &str,
1128        sources: Vec<String>,
1129        names: Vec<String>,
1130        file: Option<String>,
1131        source_root: Option<String>,
1132        sources_content: Vec<Option<String>>,
1133        ignore_list: Vec<u32>,
1134        debug_id: Option<String>,
1135        range_mappings_str: Option<&str>,
1136    ) -> Result<Self, ParseError> {
1137        let (mut mappings, line_offsets) = decode_mappings(mappings_str)?;
1138        if let Some(rm_str) = range_mappings_str
1139            && !rm_str.is_empty()
1140        {
1141            decode_range_mappings(rm_str, &mut mappings, &line_offsets)?;
1142        }
1143        let source_map = build_source_map(&sources);
1144        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1145        Ok(Self {
1146            file,
1147            source_root,
1148            sources,
1149            sources_content,
1150            names,
1151            ignore_list,
1152            extensions: HashMap::new(),
1153            debug_id,
1154            scopes: None,
1155            mappings,
1156            line_offsets,
1157            reverse_index: OnceCell::new(),
1158            source_map,
1159            has_range_mappings,
1160        })
1161    }
1162
1163    /// Create a builder for incrementally constructing a `SourceMap`.
1164    ///
1165    /// The builder accepts iterators for sources, names, and mappings,
1166    /// avoiding the need to pre-collect into `Vec`s.
1167    ///
1168    /// ```
1169    /// use srcmap_sourcemap::{SourceMap, Mapping};
1170    ///
1171    /// let sm = SourceMap::builder()
1172    ///     .file("output.js")
1173    ///     .sources(["input.ts"])
1174    ///     .sources_content([Some("let x = 1;")])
1175    ///     .names(["x"])
1176    ///     .mappings([Mapping {
1177    ///         generated_line: 0,
1178    ///         generated_column: 0,
1179    ///         source: 0,
1180    ///         original_line: 0,
1181    ///         original_column: 0,
1182    ///         name: 0,
1183    ///         is_range_mapping: false,
1184    ///     }])
1185    ///     .build();
1186    ///
1187    /// assert_eq!(sm.mapping_count(), 1);
1188    /// ```
1189    pub fn builder() -> SourceMapBuilder {
1190        SourceMapBuilder::new()
1191    }
1192
1193    /// Parse a source map from JSON, decoding only mappings for lines in `[start_line, end_line)`.
1194    ///
1195    /// This is useful for large source maps where only a subset of lines is needed.
1196    /// VLQ state is maintained through skipped lines (required for correct delta decoding),
1197    /// but `Mapping` structs are only allocated for lines in the requested range.
1198    pub fn from_json_lines(json: &str, start_line: u32, end_line: u32) -> Result<Self, ParseError> {
1199        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
1200
1201        if raw.version != 3 {
1202            return Err(ParseError::InvalidVersion(raw.version));
1203        }
1204
1205        let source_root = raw.source_root.as_deref().unwrap_or("");
1206        let sources = resolve_sources(&raw.sources, source_root);
1207        let sources_content = raw.sources_content.unwrap_or_default();
1208        let source_map = build_source_map(&sources);
1209
1210        // Decode only the requested line range
1211        let (mappings, line_offsets) = decode_mappings_range(raw.mappings, start_line, end_line)?;
1212
1213        // Decode scopes if present
1214        let num_sources = sources.len();
1215        let scopes = match raw.scopes {
1216            Some(scopes_str) if !scopes_str.is_empty() => Some(srcmap_scopes::decode_scopes(
1217                scopes_str,
1218                &raw.names,
1219                num_sources,
1220            )?),
1221            _ => None,
1222        };
1223
1224        let ignore_list = match raw.ignore_list {
1225            Some(list) => list,
1226            None => raw.x_google_ignore_list.unwrap_or_default(),
1227        };
1228
1229        // Filter extensions to only keep x_* and x-* fields
1230        let extensions: HashMap<String, serde_json::Value> = raw
1231            .extensions
1232            .into_iter()
1233            .filter(|(k, _)| k.starts_with("x_") || k.starts_with("x-"))
1234            .collect();
1235
1236        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1237
1238        Ok(Self {
1239            file: raw.file,
1240            source_root: raw.source_root,
1241            sources,
1242            sources_content,
1243            names: raw.names,
1244            ignore_list,
1245            extensions,
1246            debug_id: raw.debug_id,
1247            scopes,
1248            mappings,
1249            line_offsets,
1250            reverse_index: OnceCell::new(),
1251            source_map,
1252            has_range_mappings,
1253        })
1254    }
1255
1256    /// Encode all mappings back to a VLQ mappings string.
1257    pub fn encode_mappings(&self) -> String {
1258        if self.mappings.is_empty() {
1259            return String::new();
1260        }
1261
1262        let mut out: Vec<u8> = Vec::with_capacity(self.mappings.len() * 6);
1263
1264        let mut prev_gen_col: i64 = 0;
1265        let mut prev_source: i64 = 0;
1266        let mut prev_orig_line: i64 = 0;
1267        let mut prev_orig_col: i64 = 0;
1268        let mut prev_name: i64 = 0;
1269        let mut prev_gen_line: u32 = 0;
1270        let mut first_in_line = true;
1271
1272        for m in &self.mappings {
1273            while prev_gen_line < m.generated_line {
1274                out.push(b';');
1275                prev_gen_line += 1;
1276                prev_gen_col = 0;
1277                first_in_line = true;
1278            }
1279
1280            if !first_in_line {
1281                out.push(b',');
1282            }
1283            first_in_line = false;
1284
1285            srcmap_codec::vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
1286            prev_gen_col = m.generated_column as i64;
1287
1288            if m.source != NO_SOURCE {
1289                srcmap_codec::vlq_encode(&mut out, m.source as i64 - prev_source);
1290                prev_source = m.source as i64;
1291
1292                srcmap_codec::vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
1293                prev_orig_line = m.original_line as i64;
1294
1295                srcmap_codec::vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
1296                prev_orig_col = m.original_column as i64;
1297
1298                if m.name != NO_NAME {
1299                    srcmap_codec::vlq_encode(&mut out, m.name as i64 - prev_name);
1300                    prev_name = m.name as i64;
1301                }
1302            }
1303        }
1304
1305        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
1306        // and we only add b';' and b',' — all valid UTF-8.
1307        debug_assert!(out.is_ascii());
1308        unsafe { String::from_utf8_unchecked(out) }
1309    }
1310
1311    pub fn encode_range_mappings(&self) -> Option<String> {
1312        if !self.has_range_mappings {
1313            return None;
1314        }
1315        let line_count = self.line_offsets.len().saturating_sub(1);
1316        let mut out: Vec<u8> = Vec::new();
1317        for line_idx in 0..line_count {
1318            if line_idx > 0 {
1319                out.push(b';');
1320            }
1321            let start = self.line_offsets[line_idx] as usize;
1322            let end = self.line_offsets[line_idx + 1] as usize;
1323            let mut prev_offset: u64 = 0;
1324            let mut first_on_line = true;
1325            for (i, mapping) in self.mappings[start..end].iter().enumerate() {
1326                if mapping.is_range_mapping {
1327                    if !first_on_line {
1328                        out.push(b',');
1329                    }
1330                    first_on_line = false;
1331                    vlq_encode_unsigned(&mut out, i as u64 - prev_offset);
1332                    prev_offset = i as u64;
1333                }
1334            }
1335        }
1336        while out.last() == Some(&b';') {
1337            out.pop();
1338        }
1339        if out.is_empty() {
1340            return None;
1341        }
1342        // SAFETY: vlq_encode_unsigned only pushes ASCII base64 chars,
1343        // and we only add b';' and b',' — all valid UTF-8.
1344        debug_assert!(out.is_ascii());
1345        Some(unsafe { String::from_utf8_unchecked(out) })
1346    }
1347
1348    #[inline]
1349    pub fn has_range_mappings(&self) -> bool {
1350        self.has_range_mappings
1351    }
1352
1353    #[inline]
1354    pub fn range_mapping_count(&self) -> usize {
1355        self.mappings.iter().filter(|m| m.is_range_mapping).count()
1356    }
1357}
1358
1359// ── LazySourceMap ──────────────────────────────────────────────────
1360
1361/// Cumulative VLQ state at a line boundary.
1362#[derive(Debug, Clone, Copy)]
1363struct VlqState {
1364    source_index: i64,
1365    original_line: i64,
1366    original_column: i64,
1367    name_index: i64,
1368}
1369
1370/// Pre-scanned line info for O(1) random access into the raw mappings string.
1371#[derive(Debug, Clone)]
1372struct LineInfo {
1373    /// Byte offset into the raw mappings string where this line starts.
1374    byte_offset: usize,
1375    /// Byte offset where this line ends (exclusive, at `;` or end of string).
1376    byte_end: usize,
1377    /// Cumulative VLQ state at the start of this line.
1378    state: VlqState,
1379}
1380
1381/// A lazily-decoded source map that defers VLQ mappings decoding until needed.
1382///
1383/// For large source maps (100MB+), this avoids decoding all mappings upfront.
1384/// JSON metadata (sources, names, etc.) is parsed eagerly, but VLQ mappings
1385/// are decoded on a per-line basis on demand.
1386///
1387/// # Examples
1388///
1389/// ```
1390/// use srcmap_sourcemap::LazySourceMap;
1391///
1392/// let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
1393/// let sm = LazySourceMap::from_json(json).unwrap();
1394///
1395/// // Mappings are only decoded when accessed
1396/// let loc = sm.original_position_for(0, 0).unwrap();
1397/// assert_eq!(sm.source(loc.source), "input.js");
1398/// ```
1399#[derive(Debug)]
1400pub struct LazySourceMap {
1401    pub file: Option<String>,
1402    pub source_root: Option<String>,
1403    pub sources: Vec<String>,
1404    pub sources_content: Vec<Option<String>>,
1405    pub names: Vec<String>,
1406    pub ignore_list: Vec<u32>,
1407    pub extensions: HashMap<String, serde_json::Value>,
1408    pub debug_id: Option<String>,
1409    pub scopes: Option<ScopeInfo>,
1410
1411    /// Raw VLQ mappings string (owned).
1412    raw_mappings: String,
1413
1414    /// Pre-scanned line info for O(1) line access.
1415    line_info: Vec<LineInfo>,
1416
1417    /// Cache of decoded lines: line index -> `Vec<Mapping>`.
1418    decoded_lines: RefCell<HashMap<u32, Vec<Mapping>>>,
1419
1420    /// Source filename -> index for O(1) lookup by name.
1421    source_map: HashMap<String, u32>,
1422}
1423
1424impl LazySourceMap {
1425    /// Parse a source map from JSON, deferring VLQ mappings decoding.
1426    ///
1427    /// Parses all JSON metadata eagerly but stores the raw mappings string.
1428    /// VLQ mappings are decoded per-line on demand.
1429    pub fn from_json(json: &str) -> Result<Self, ParseError> {
1430        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
1431
1432        if raw.version != 3 {
1433            return Err(ParseError::InvalidVersion(raw.version));
1434        }
1435
1436        let source_root = raw.source_root.as_deref().unwrap_or("");
1437        let sources = resolve_sources(&raw.sources, source_root);
1438        let sources_content = raw.sources_content.unwrap_or_default();
1439        let source_map = build_source_map(&sources);
1440
1441        // Pre-scan the raw mappings string to find semicolon positions
1442        // and compute cumulative VLQ state at each line boundary.
1443        let raw_mappings = raw.mappings.to_string();
1444        let line_info = prescan_mappings(&raw_mappings)?;
1445
1446        // Decode scopes if present
1447        let num_sources = sources.len();
1448        let scopes = match raw.scopes {
1449            Some(scopes_str) if !scopes_str.is_empty() => Some(srcmap_scopes::decode_scopes(
1450                scopes_str,
1451                &raw.names,
1452                num_sources,
1453            )?),
1454            _ => None,
1455        };
1456
1457        let ignore_list = match raw.ignore_list {
1458            Some(list) => list,
1459            None => raw.x_google_ignore_list.unwrap_or_default(),
1460        };
1461
1462        // Filter extensions to only keep x_* and x-* fields
1463        let extensions: HashMap<String, serde_json::Value> = raw
1464            .extensions
1465            .into_iter()
1466            .filter(|(k, _)| k.starts_with("x_") || k.starts_with("x-"))
1467            .collect();
1468
1469        Ok(Self {
1470            file: raw.file,
1471            source_root: raw.source_root,
1472            sources,
1473            sources_content,
1474            names: raw.names,
1475            ignore_list,
1476            extensions,
1477            debug_id: raw.debug_id,
1478            scopes,
1479            raw_mappings,
1480            line_info,
1481            decoded_lines: RefCell::new(HashMap::new()),
1482            source_map,
1483        })
1484    }
1485
1486    /// Decode a single line's mappings on demand.
1487    ///
1488    /// Returns the cached result if the line has already been decoded.
1489    /// The line index is 0-based.
1490    pub fn decode_line(&self, line: u32) -> Result<Vec<Mapping>, DecodeError> {
1491        // Check cache first
1492        if let Some(cached) = self.decoded_lines.borrow().get(&line) {
1493            return Ok(cached.clone());
1494        }
1495
1496        let line_idx = line as usize;
1497        if line_idx >= self.line_info.len() {
1498            return Ok(Vec::new());
1499        }
1500
1501        let info = &self.line_info[line_idx];
1502        let bytes = self.raw_mappings.as_bytes();
1503        let slice = &bytes[info.byte_offset..info.byte_end];
1504
1505        let mut mappings = Vec::new();
1506        let mut source_index = info.state.source_index;
1507        let mut original_line = info.state.original_line;
1508        let mut original_column = info.state.original_column;
1509        let mut name_index = info.state.name_index;
1510        let mut generated_column: i64 = 0;
1511        let mut pos: usize = 0;
1512        let len = slice.len();
1513
1514        // We need to adjust `pos` for vlq_fast which works on the full byte slice.
1515        // Instead, create a local helper working on the slice directly.
1516        let base_offset = info.byte_offset;
1517
1518        while pos < len {
1519            let byte = slice[pos];
1520
1521            if byte == b',' {
1522                pos += 1;
1523                continue;
1524            }
1525
1526            // Use vlq_fast on the full byte buffer with adjusted position
1527            let mut abs_pos = base_offset + pos;
1528
1529            // Field 1: generated column
1530            generated_column += vlq_fast(bytes, &mut abs_pos)?;
1531
1532            if abs_pos < base_offset + len && bytes[abs_pos] != b',' && bytes[abs_pos] != b';' {
1533                // Field 2: source index
1534                source_index += vlq_fast(bytes, &mut abs_pos)?;
1535
1536                // Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
1537                if abs_pos >= base_offset + len || bytes[abs_pos] == b',' || bytes[abs_pos] == b';'
1538                {
1539                    return Err(DecodeError::InvalidSegmentLength {
1540                        fields: 2,
1541                        offset: abs_pos,
1542                    });
1543                }
1544
1545                // Field 3: original line
1546                original_line += vlq_fast(bytes, &mut abs_pos)?;
1547
1548                // Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
1549                if abs_pos >= base_offset + len || bytes[abs_pos] == b',' || bytes[abs_pos] == b';'
1550                {
1551                    return Err(DecodeError::InvalidSegmentLength {
1552                        fields: 3,
1553                        offset: abs_pos,
1554                    });
1555                }
1556
1557                // Field 4: original column
1558                original_column += vlq_fast(bytes, &mut abs_pos)?;
1559
1560                // Field 5: name (optional)
1561                let name = if abs_pos < base_offset + len
1562                    && bytes[abs_pos] != b','
1563                    && bytes[abs_pos] != b';'
1564                {
1565                    name_index += vlq_fast(bytes, &mut abs_pos)?;
1566                    name_index as u32
1567                } else {
1568                    NO_NAME
1569                };
1570
1571                mappings.push(Mapping {
1572                    generated_line: line,
1573                    generated_column: generated_column as u32,
1574                    source: source_index as u32,
1575                    original_line: original_line as u32,
1576                    original_column: original_column as u32,
1577                    name,
1578                    is_range_mapping: false,
1579                });
1580            } else {
1581                // 1-field segment
1582                mappings.push(Mapping {
1583                    generated_line: line,
1584                    generated_column: generated_column as u32,
1585                    source: NO_SOURCE,
1586                    original_line: 0,
1587                    original_column: 0,
1588                    name: NO_NAME,
1589                    is_range_mapping: false,
1590                });
1591            }
1592
1593            pos = abs_pos - base_offset;
1594        }
1595
1596        // Cache the result
1597        self.decoded_lines
1598            .borrow_mut()
1599            .insert(line, mappings.clone());
1600
1601        Ok(mappings)
1602    }
1603
1604    /// Look up the original source position for a generated position.
1605    ///
1606    /// Both `line` and `column` are 0-based.
1607    /// Returns `None` if no mapping exists or the mapping has no source.
1608    pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
1609        let line_mappings = self.decode_line(line).ok()?;
1610
1611        if line_mappings.is_empty() {
1612            return None;
1613        }
1614
1615        // Binary search for greatest lower bound
1616        let idx = match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
1617            Ok(i) => i,
1618            Err(0) => return None,
1619            Err(i) => i - 1,
1620        };
1621
1622        let mapping = &line_mappings[idx];
1623
1624        if mapping.source == NO_SOURCE {
1625            return None;
1626        }
1627
1628        Some(OriginalLocation {
1629            source: mapping.source,
1630            line: mapping.original_line,
1631            column: mapping.original_column,
1632            name: if mapping.name == NO_NAME {
1633                None
1634            } else {
1635                Some(mapping.name)
1636            },
1637        })
1638    }
1639
1640    /// Number of generated lines in the source map.
1641    #[inline]
1642    pub fn line_count(&self) -> usize {
1643        self.line_info.len()
1644    }
1645
1646    /// Resolve a source index to its filename.
1647    ///
1648    /// # Panics
1649    ///
1650    /// Panics if `index` is out of bounds. Use [`get_source`](Self::get_source)
1651    /// for a non-panicking alternative.
1652    #[inline]
1653    pub fn source(&self, index: u32) -> &str {
1654        &self.sources[index as usize]
1655    }
1656
1657    /// Resolve a source index to its filename, returning `None` if out of bounds.
1658    #[inline]
1659    pub fn get_source(&self, index: u32) -> Option<&str> {
1660        self.sources.get(index as usize).map(|s| s.as_str())
1661    }
1662
1663    /// Resolve a name index to its string.
1664    ///
1665    /// # Panics
1666    ///
1667    /// Panics if `index` is out of bounds. Use [`get_name`](Self::get_name)
1668    /// for a non-panicking alternative.
1669    #[inline]
1670    pub fn name(&self, index: u32) -> &str {
1671        &self.names[index as usize]
1672    }
1673
1674    /// Resolve a name index to its string, returning `None` if out of bounds.
1675    #[inline]
1676    pub fn get_name(&self, index: u32) -> Option<&str> {
1677        self.names.get(index as usize).map(|s| s.as_str())
1678    }
1679
1680    /// Find the source index for a filename.
1681    #[inline]
1682    pub fn source_index(&self, name: &str) -> Option<u32> {
1683        self.source_map.get(name).copied()
1684    }
1685
1686    /// Get all mappings for a line (decoding on demand).
1687    pub fn mappings_for_line(&self, line: u32) -> Vec<Mapping> {
1688        self.decode_line(line).unwrap_or_default()
1689    }
1690
1691    /// Fully decode all mappings into a regular `SourceMap`.
1692    ///
1693    /// Useful when you need the full map after lazy exploration.
1694    pub fn into_sourcemap(self) -> Result<SourceMap, ParseError> {
1695        let (mappings, line_offsets) = decode_mappings(&self.raw_mappings)?;
1696        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1697
1698        Ok(SourceMap {
1699            file: self.file,
1700            source_root: self.source_root,
1701            sources: self.sources.clone(),
1702            sources_content: self.sources_content,
1703            names: self.names,
1704            ignore_list: self.ignore_list,
1705            extensions: self.extensions,
1706            debug_id: self.debug_id,
1707            scopes: self.scopes,
1708            mappings,
1709            line_offsets,
1710            reverse_index: OnceCell::new(),
1711            source_map: self.source_map,
1712            has_range_mappings,
1713        })
1714    }
1715}
1716
1717/// Pre-scan the raw mappings string to find semicolon positions and compute
1718/// cumulative VLQ state at each line boundary.
1719fn prescan_mappings(input: &str) -> Result<Vec<LineInfo>, DecodeError> {
1720    if input.is_empty() {
1721        return Ok(Vec::new());
1722    }
1723
1724    let bytes = input.as_bytes();
1725    let len = bytes.len();
1726
1727    // Count lines for pre-allocation
1728    let line_count = bytes.iter().filter(|&&b| b == b';').count() + 1;
1729    let mut line_info: Vec<LineInfo> = Vec::with_capacity(line_count);
1730
1731    let mut source_index: i64 = 0;
1732    let mut original_line: i64 = 0;
1733    let mut original_column: i64 = 0;
1734    let mut name_index: i64 = 0;
1735    let mut pos: usize = 0;
1736
1737    loop {
1738        let line_start = pos;
1739        let state = VlqState {
1740            source_index,
1741            original_line,
1742            original_column,
1743            name_index,
1744        };
1745
1746        let mut saw_semicolon = false;
1747
1748        // Walk the VLQ data, updating cumulative state but not allocating mappings
1749        while pos < len {
1750            let byte = bytes[pos];
1751
1752            if byte == b';' {
1753                pos += 1;
1754                saw_semicolon = true;
1755                break;
1756            }
1757
1758            if byte == b',' {
1759                pos += 1;
1760                continue;
1761            }
1762
1763            // Field 1: generated column (skip value, it resets per line)
1764            vlq_fast(bytes, &mut pos)?;
1765
1766            if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
1767                // Field 2: source index
1768                source_index += vlq_fast(bytes, &mut pos)?;
1769
1770                // Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
1771                if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
1772                    return Err(DecodeError::InvalidSegmentLength {
1773                        fields: 2,
1774                        offset: pos,
1775                    });
1776                }
1777
1778                // Field 3: original line
1779                original_line += vlq_fast(bytes, &mut pos)?;
1780
1781                // Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
1782                if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
1783                    return Err(DecodeError::InvalidSegmentLength {
1784                        fields: 3,
1785                        offset: pos,
1786                    });
1787                }
1788
1789                // Field 4: original column
1790                original_column += vlq_fast(bytes, &mut pos)?;
1791
1792                // Field 5: name (optional)
1793                if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
1794                    name_index += vlq_fast(bytes, &mut pos)?;
1795                }
1796            }
1797        }
1798
1799        // byte_end is before the semicolon (or end of string)
1800        let byte_end = if saw_semicolon { pos - 1 } else { pos };
1801
1802        line_info.push(LineInfo {
1803            byte_offset: line_start,
1804            byte_end,
1805            state,
1806        });
1807
1808        if !saw_semicolon {
1809            break;
1810        }
1811    }
1812
1813    Ok(line_info)
1814}
1815
1816/// Result of parsing a sourceMappingURL reference.
1817#[derive(Debug, Clone, PartialEq, Eq)]
1818pub enum SourceMappingUrl {
1819    /// An inline base64 data URI containing the source map JSON.
1820    Inline(String),
1821    /// An external URL or relative path to the source map file.
1822    External(String),
1823}
1824
1825/// Extract the sourceMappingURL from generated source code.
1826///
1827/// Looks for `//# sourceMappingURL=<url>` or `//@ sourceMappingURL=<url>` comments.
1828/// For inline data URIs (`data:application/json;base64,...`), decodes the base64 content.
1829/// Returns `None` if no sourceMappingURL is found.
1830pub fn parse_source_mapping_url(source: &str) -> Option<SourceMappingUrl> {
1831    // Search backwards from the end (sourceMappingURL is typically the last line)
1832    for line in source.lines().rev() {
1833        let trimmed = line.trim();
1834        let url = if let Some(rest) = trimmed.strip_prefix("//# sourceMappingURL=") {
1835            rest.trim()
1836        } else if let Some(rest) = trimmed.strip_prefix("//@ sourceMappingURL=") {
1837            rest.trim()
1838        } else if let Some(rest) = trimmed.strip_prefix("/*# sourceMappingURL=") {
1839            rest.trim_end_matches("*/").trim()
1840        } else if let Some(rest) = trimmed.strip_prefix("/*@ sourceMappingURL=") {
1841            rest.trim_end_matches("*/").trim()
1842        } else {
1843            continue;
1844        };
1845
1846        if url.is_empty() {
1847            continue;
1848        }
1849
1850        // Check for inline data URI
1851        if let Some(base64_data) = url
1852            .strip_prefix("data:application/json;base64,")
1853            .or_else(|| url.strip_prefix("data:application/json;charset=utf-8;base64,"))
1854            .or_else(|| url.strip_prefix("data:application/json;charset=UTF-8;base64,"))
1855        {
1856            // Decode base64
1857            let decoded = base64_decode(base64_data);
1858            if let Some(json) = decoded {
1859                return Some(SourceMappingUrl::Inline(json));
1860            }
1861        }
1862
1863        return Some(SourceMappingUrl::External(url.to_string()));
1864    }
1865
1866    None
1867}
1868
1869/// Simple base64 decoder (no dependencies).
1870fn base64_decode(input: &str) -> Option<String> {
1871    let input = input.trim();
1872    let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
1873
1874    let mut output = Vec::with_capacity(bytes.len() * 3 / 4);
1875
1876    for chunk in bytes.chunks(4) {
1877        let mut buf = [0u8; 4];
1878        let mut len = 0;
1879
1880        for &b in chunk {
1881            if b == b'=' {
1882                break;
1883            }
1884            let val = match b {
1885                b'A'..=b'Z' => b - b'A',
1886                b'a'..=b'z' => b - b'a' + 26,
1887                b'0'..=b'9' => b - b'0' + 52,
1888                b'+' => 62,
1889                b'/' => 63,
1890                _ => return None,
1891            };
1892            buf[len] = val;
1893            len += 1;
1894        }
1895
1896        if len >= 2 {
1897            output.push((buf[0] << 2) | (buf[1] >> 4));
1898        }
1899        if len >= 3 {
1900            output.push((buf[1] << 4) | (buf[2] >> 2));
1901        }
1902        if len >= 4 {
1903            output.push((buf[2] << 6) | buf[3]);
1904        }
1905    }
1906
1907    String::from_utf8(output).ok()
1908}
1909
1910/// Validate a source map with deep structural checks.
1911///
1912/// Performs bounds checking, segment ordering verification, source resolution,
1913/// and unreferenced sources detection beyond basic JSON parsing.
1914pub fn validate_deep(sm: &SourceMap) -> Vec<String> {
1915    let mut warnings = Vec::new();
1916
1917    // Check segment ordering (must be sorted by generated position)
1918    let mut prev_line: u32 = 0;
1919    let mut prev_col: u32 = 0;
1920    let mappings = sm.all_mappings();
1921    for m in mappings {
1922        if m.generated_line < prev_line
1923            || (m.generated_line == prev_line && m.generated_column < prev_col)
1924        {
1925            warnings.push(format!(
1926                "mappings out of order at {}:{}",
1927                m.generated_line, m.generated_column
1928            ));
1929        }
1930        prev_line = m.generated_line;
1931        prev_col = m.generated_column;
1932    }
1933
1934    // Check source indices in bounds
1935    for m in mappings {
1936        if m.source != NO_SOURCE && m.source as usize >= sm.sources.len() {
1937            warnings.push(format!(
1938                "source index {} out of bounds (max {})",
1939                m.source,
1940                sm.sources.len()
1941            ));
1942        }
1943        if m.name != NO_NAME && m.name as usize >= sm.names.len() {
1944            warnings.push(format!(
1945                "name index {} out of bounds (max {})",
1946                m.name,
1947                sm.names.len()
1948            ));
1949        }
1950    }
1951
1952    // Check ignoreList indices in bounds
1953    for &idx in &sm.ignore_list {
1954        if idx as usize >= sm.sources.len() {
1955            warnings.push(format!(
1956                "ignoreList index {} out of bounds (max {})",
1957                idx,
1958                sm.sources.len()
1959            ));
1960        }
1961    }
1962
1963    // Detect unreferenced sources
1964    let mut referenced_sources = std::collections::HashSet::new();
1965    for m in mappings {
1966        if m.source != NO_SOURCE {
1967            referenced_sources.insert(m.source);
1968        }
1969    }
1970    for (i, source) in sm.sources.iter().enumerate() {
1971        if !referenced_sources.contains(&(i as u32)) {
1972            warnings.push(format!("source \"{source}\" (index {i}) is unreferenced"));
1973        }
1974    }
1975
1976    warnings
1977}
1978
1979/// Append a JSON-quoted string to the output buffer.
1980fn json_quote_into(out: &mut String, s: &str) {
1981    let bytes = s.as_bytes();
1982    out.push('"');
1983
1984    let mut start = 0;
1985    for (i, &b) in bytes.iter().enumerate() {
1986        let escape = match b {
1987            b'"' => "\\\"",
1988            b'\\' => "\\\\",
1989            b'\n' => "\\n",
1990            b'\r' => "\\r",
1991            b'\t' => "\\t",
1992            0x00..=0x1f => {
1993                if start < i {
1994                    out.push_str(&s[start..i]);
1995                }
1996                use std::fmt::Write;
1997                let _ = write!(out, "\\u{:04x}", b);
1998                start = i + 1;
1999                continue;
2000            }
2001            _ => continue,
2002        };
2003        if start < i {
2004            out.push_str(&s[start..i]);
2005        }
2006        out.push_str(escape);
2007        start = i + 1;
2008    }
2009
2010    if start < bytes.len() {
2011        out.push_str(&s[start..]);
2012    }
2013
2014    out.push('"');
2015}
2016
2017// ── Internal: decode VLQ mappings directly into flat Mapping vec ───
2018
2019/// Base64 decode lookup table (byte → 6-bit value, 0xFF = invalid).
2020const B64: [u8; 128] = {
2021    let mut table = [0xFFu8; 128];
2022    let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2023    let mut i = 0u8;
2024    while i < 64 {
2025        table[chars[i as usize] as usize] = i;
2026        i += 1;
2027    }
2028    table
2029};
2030
2031/// Inline VLQ decode optimized for the hot path (no function call overhead).
2032/// Most source map VLQ values fit in 1-2 base64 characters.
2033#[inline(always)]
2034fn vlq_fast(bytes: &[u8], pos: &mut usize) -> Result<i64, DecodeError> {
2035    let p = *pos;
2036    if p >= bytes.len() {
2037        return Err(DecodeError::UnexpectedEof { offset: p });
2038    }
2039
2040    let b0 = bytes[p];
2041    if b0 >= 128 {
2042        return Err(DecodeError::InvalidBase64 {
2043            byte: b0,
2044            offset: p,
2045        });
2046    }
2047    let d0 = B64[b0 as usize];
2048    if d0 == 0xFF {
2049        return Err(DecodeError::InvalidBase64 {
2050            byte: b0,
2051            offset: p,
2052        });
2053    }
2054
2055    // Fast path: single character VLQ (values -15..15)
2056    if (d0 & 0x20) == 0 {
2057        *pos = p + 1;
2058        let val = (d0 >> 1) as i64;
2059        return Ok(if (d0 & 1) != 0 { -val } else { val });
2060    }
2061
2062    // Multi-character VLQ
2063    let mut result: u64 = (d0 & 0x1F) as u64;
2064    let mut shift: u32 = 5;
2065    let mut i = p + 1;
2066
2067    loop {
2068        if i >= bytes.len() {
2069            return Err(DecodeError::UnexpectedEof { offset: i });
2070        }
2071        let b = bytes[i];
2072        if b >= 128 {
2073            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2074        }
2075        let d = B64[b as usize];
2076        if d == 0xFF {
2077            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2078        }
2079        i += 1;
2080
2081        if shift >= 64 {
2082            return Err(DecodeError::VlqOverflow { offset: p });
2083        }
2084
2085        result += ((d & 0x1F) as u64) << shift;
2086        shift += 5;
2087
2088        if (d & 0x20) == 0 {
2089            break;
2090        }
2091    }
2092
2093    *pos = i;
2094    let value = if (result & 1) == 1 {
2095        -((result >> 1) as i64)
2096    } else {
2097        (result >> 1) as i64
2098    };
2099    Ok(value)
2100}
2101
2102#[inline(always)]
2103fn vlq_unsigned_fast(bytes: &[u8], pos: &mut usize) -> Result<u64, DecodeError> {
2104    let p = *pos;
2105    if p >= bytes.len() {
2106        return Err(DecodeError::UnexpectedEof { offset: p });
2107    }
2108    let b0 = bytes[p];
2109    if b0 >= 128 {
2110        return Err(DecodeError::InvalidBase64 {
2111            byte: b0,
2112            offset: p,
2113        });
2114    }
2115    let d0 = B64[b0 as usize];
2116    if d0 == 0xFF {
2117        return Err(DecodeError::InvalidBase64 {
2118            byte: b0,
2119            offset: p,
2120        });
2121    }
2122    if (d0 & 0x20) == 0 {
2123        *pos = p + 1;
2124        return Ok(d0 as u64);
2125    }
2126    let mut result: u64 = (d0 & 0x1F) as u64;
2127    let mut shift: u32 = 5;
2128    let mut i = p + 1;
2129    loop {
2130        if i >= bytes.len() {
2131            return Err(DecodeError::UnexpectedEof { offset: i });
2132        }
2133        let b = bytes[i];
2134        if b >= 128 {
2135            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2136        }
2137        let d = B64[b as usize];
2138        if d == 0xFF {
2139            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2140        }
2141        i += 1;
2142        if shift >= 64 {
2143            return Err(DecodeError::VlqOverflow { offset: p });
2144        }
2145        result |= ((d & 0x1F) as u64) << shift;
2146        shift += 5;
2147        if (d & 0x20) == 0 {
2148            break;
2149        }
2150    }
2151    *pos = i;
2152    Ok(result)
2153}
2154
2155fn decode_range_mappings(
2156    input: &str,
2157    mappings: &mut [Mapping],
2158    line_offsets: &[u32],
2159) -> Result<(), DecodeError> {
2160    let bytes = input.as_bytes();
2161    let len = bytes.len();
2162    let mut pos: usize = 0;
2163    let mut generated_line: usize = 0;
2164    while pos < len {
2165        let line_start = if generated_line + 1 < line_offsets.len() {
2166            line_offsets[generated_line] as usize
2167        } else {
2168            break;
2169        };
2170        let mut mapping_index: u64 = 0;
2171        while pos < len {
2172            let byte = bytes[pos];
2173            if byte == b';' {
2174                pos += 1;
2175                break;
2176            }
2177            if byte == b',' {
2178                pos += 1;
2179                continue;
2180            }
2181            let offset = vlq_unsigned_fast(bytes, &mut pos)?;
2182            mapping_index += offset;
2183            let abs_idx = line_start + mapping_index as usize;
2184            if abs_idx < mappings.len() {
2185                mappings[abs_idx].is_range_mapping = true;
2186            }
2187        }
2188        generated_line += 1;
2189    }
2190    Ok(())
2191}
2192
2193fn decode_mappings(input: &str) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
2194    if input.is_empty() {
2195        return Ok((Vec::new(), vec![0]));
2196    }
2197
2198    let bytes = input.as_bytes();
2199    let len = bytes.len();
2200
2201    // Pre-count lines and segments in a single pass for capacity hints
2202    let mut semicolons = 0usize;
2203    let mut commas = 0usize;
2204    for &b in bytes {
2205        semicolons += (b == b';') as usize;
2206        commas += (b == b',') as usize;
2207    }
2208    let line_count = semicolons + 1;
2209    let approx_segments = commas + line_count;
2210
2211    let mut mappings: Vec<Mapping> = Vec::with_capacity(approx_segments);
2212    let mut line_offsets: Vec<u32> = Vec::with_capacity(line_count + 1);
2213
2214    let mut source_index: i64 = 0;
2215    let mut original_line: i64 = 0;
2216    let mut original_column: i64 = 0;
2217    let mut name_index: i64 = 0;
2218    let mut generated_line: u32 = 0;
2219    let mut pos: usize = 0;
2220
2221    loop {
2222        line_offsets.push(mappings.len() as u32);
2223        let mut generated_column: i64 = 0;
2224        let mut saw_semicolon = false;
2225
2226        while pos < len {
2227            let byte = bytes[pos];
2228
2229            if byte == b';' {
2230                pos += 1;
2231                saw_semicolon = true;
2232                break;
2233            }
2234
2235            if byte == b',' {
2236                pos += 1;
2237                continue;
2238            }
2239
2240            // Field 1: generated column
2241            generated_column += vlq_fast(bytes, &mut pos)?;
2242
2243            if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
2244                // Field 2: source index
2245                source_index += vlq_fast(bytes, &mut pos)?;
2246
2247                // Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
2248                if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
2249                    return Err(DecodeError::InvalidSegmentLength {
2250                        fields: 2,
2251                        offset: pos,
2252                    });
2253                }
2254
2255                // Field 3: original line
2256                original_line += vlq_fast(bytes, &mut pos)?;
2257
2258                // Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
2259                if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
2260                    return Err(DecodeError::InvalidSegmentLength {
2261                        fields: 3,
2262                        offset: pos,
2263                    });
2264                }
2265
2266                // Field 4: original column
2267                original_column += vlq_fast(bytes, &mut pos)?;
2268
2269                // Field 5: name (optional)
2270                let name = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
2271                    name_index += vlq_fast(bytes, &mut pos)?;
2272                    name_index as u32
2273                } else {
2274                    NO_NAME
2275                };
2276
2277                mappings.push(Mapping {
2278                    generated_line,
2279                    generated_column: generated_column as u32,
2280                    source: source_index as u32,
2281                    original_line: original_line as u32,
2282                    original_column: original_column as u32,
2283                    name,
2284                    is_range_mapping: false,
2285                });
2286            } else {
2287                // 1-field segment: no source info
2288                mappings.push(Mapping {
2289                    generated_line,
2290                    generated_column: generated_column as u32,
2291                    source: NO_SOURCE,
2292                    original_line: 0,
2293                    original_column: 0,
2294                    name: NO_NAME,
2295                    is_range_mapping: false,
2296                });
2297            }
2298        }
2299
2300        if !saw_semicolon {
2301            break;
2302        }
2303        generated_line += 1;
2304    }
2305
2306    // Sentinel for line range computation
2307    line_offsets.push(mappings.len() as u32);
2308
2309    Ok((mappings, line_offsets))
2310}
2311
2312/// Decode VLQ mappings for a subset of lines `[start_line, end_line)`.
2313///
2314/// Walks VLQ state for all lines up to `end_line`, but only allocates Mapping
2315/// structs for lines in the requested range. The returned `line_offsets` is
2316/// indexed by the actual generated line number (not relative to start_line),
2317/// so that `mappings_for_line(line)` works correctly with the real line values.
2318fn decode_mappings_range(
2319    input: &str,
2320    start_line: u32,
2321    end_line: u32,
2322) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
2323    if input.is_empty() || start_line >= end_line {
2324        return Ok((Vec::new(), vec![0; end_line as usize + 1]));
2325    }
2326
2327    let bytes = input.as_bytes();
2328    let len = bytes.len();
2329
2330    let mut mappings: Vec<Mapping> = Vec::new();
2331
2332    let mut source_index: i64 = 0;
2333    let mut original_line: i64 = 0;
2334    let mut original_column: i64 = 0;
2335    let mut name_index: i64 = 0;
2336    let mut generated_line: u32 = 0;
2337    let mut pos: usize = 0;
2338
2339    // Track which line each mapping starts at, so we can build line_offsets after
2340    // We use a vec of (line, mapping_start_index) pairs for lines in range
2341    let mut line_starts: Vec<(u32, u32)> = Vec::new();
2342
2343    loop {
2344        let in_range = generated_line >= start_line && generated_line < end_line;
2345        if in_range {
2346            line_starts.push((generated_line, mappings.len() as u32));
2347        }
2348
2349        let mut generated_column: i64 = 0;
2350        let mut saw_semicolon = false;
2351
2352        while pos < len {
2353            let byte = bytes[pos];
2354
2355            if byte == b';' {
2356                pos += 1;
2357                saw_semicolon = true;
2358                break;
2359            }
2360
2361            if byte == b',' {
2362                pos += 1;
2363                continue;
2364            }
2365
2366            // Field 1: generated column
2367            generated_column += vlq_fast(bytes, &mut pos)?;
2368
2369            if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
2370                // Field 2: source index
2371                source_index += vlq_fast(bytes, &mut pos)?;
2372
2373                // Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
2374                if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
2375                    return Err(DecodeError::InvalidSegmentLength {
2376                        fields: 2,
2377                        offset: pos,
2378                    });
2379                }
2380
2381                // Field 3: original line
2382                original_line += vlq_fast(bytes, &mut pos)?;
2383
2384                // Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
2385                if pos >= len || bytes[pos] == b',' || bytes[pos] == b';' {
2386                    return Err(DecodeError::InvalidSegmentLength {
2387                        fields: 3,
2388                        offset: pos,
2389                    });
2390                }
2391
2392                // Field 4: original column
2393                original_column += vlq_fast(bytes, &mut pos)?;
2394
2395                // Field 5: name (optional)
2396                let name = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
2397                    name_index += vlq_fast(bytes, &mut pos)?;
2398                    name_index as u32
2399                } else {
2400                    NO_NAME
2401                };
2402
2403                if in_range {
2404                    mappings.push(Mapping {
2405                        generated_line,
2406                        generated_column: generated_column as u32,
2407                        source: source_index as u32,
2408                        original_line: original_line as u32,
2409                        original_column: original_column as u32,
2410                        name,
2411                        is_range_mapping: false,
2412                    });
2413                }
2414            } else {
2415                // 1-field segment: no source info
2416                if in_range {
2417                    mappings.push(Mapping {
2418                        generated_line,
2419                        generated_column: generated_column as u32,
2420                        source: NO_SOURCE,
2421                        original_line: 0,
2422                        original_column: 0,
2423                        name: NO_NAME,
2424                        is_range_mapping: false,
2425                    });
2426                }
2427            }
2428        }
2429
2430        if !saw_semicolon {
2431            break;
2432        }
2433        generated_line += 1;
2434
2435        // Stop early once we've passed end_line
2436        if generated_line >= end_line {
2437            break;
2438        }
2439    }
2440
2441    // Build line_offsets indexed by actual line number.
2442    // Size = end_line + 1 so that line_offsets[line] and line_offsets[line+1] both exist
2443    // for any line < end_line.
2444    let total = mappings.len() as u32;
2445    let mut line_offsets: Vec<u32> = vec![total; end_line as usize + 1];
2446
2447    // Fill from our recorded line starts (in reverse so gaps get forward-filled correctly)
2448    // First set lines before start_line to 0 (they have no mappings, and 0..0 = empty)
2449    for i in 0..=start_line as usize {
2450        if i < line_offsets.len() {
2451            line_offsets[i] = 0;
2452        }
2453    }
2454
2455    // Set each recorded line start
2456    for &(line, offset) in &line_starts {
2457        line_offsets[line as usize] = offset;
2458    }
2459
2460    // Forward-fill gaps within the range: if a line in [start_line, end_line) wasn't
2461    // in line_starts, it should point to the same offset as the next line with mappings.
2462    // We do a backward pass: for lines not explicitly set, they should get the offset
2463    // of the next line (which means "empty line" since start == end for that range).
2464    // Actually, the approach used in decode_mappings is forward: each line offset is
2465    // set when we encounter it. Lines not encountered get the sentinel (total).
2466    // But we also need lines before start_line to be "empty" (start == end).
2467    // Since lines < start_line all have offset 0 and line start_line also starts at 0
2468    // (or wherever the first mapping is), lines before start_line correctly have 0..0 = empty.
2469
2470    // However, there's a subtlety: if start_line has no mappings (empty line in range),
2471    // it would have been set to 0 by line_starts but then line_offsets[start_line+1]
2472    // might also be 0, making an empty range. Let's just ensure the forward-fill works:
2473    // lines within range that have no mappings should have the same offset as the next
2474    // line's start.
2475
2476    // Actually the simplest correct approach: walk forward from start_line to end_line.
2477    // For each line not in line_starts, set it to the value of the previous line's end
2478    // (which is the current mapping count at that point).
2479    // We already recorded line_starts in order, so let's use them directly.
2480
2481    // Reset line_offsets for lines in [start_line, end_line] to sentinel
2482    for i in start_line as usize..=end_line as usize {
2483        if i < line_offsets.len() {
2484            line_offsets[i] = total;
2485        }
2486    }
2487
2488    // Set recorded line starts
2489    for &(line, offset) in &line_starts {
2490        line_offsets[line as usize] = offset;
2491    }
2492
2493    // Forward-fill: lines in the range that weren't recorded should get
2494    // the offset of the next line (so they appear empty)
2495    // Walk backward from end_line to start_line
2496    let mut next_offset = total;
2497    for i in (start_line as usize..end_line as usize).rev() {
2498        if line_offsets[i] == total {
2499            // This line wasn't in the input at all, point to next_offset
2500            line_offsets[i] = next_offset;
2501        } else {
2502            next_offset = line_offsets[i];
2503        }
2504    }
2505
2506    // Lines before start_line should all be empty.
2507    // Make consecutive lines point to 0 so start == end == 0.
2508    for offset in line_offsets.iter_mut().take(start_line as usize) {
2509        *offset = 0;
2510    }
2511
2512    Ok((mappings, line_offsets))
2513}
2514
2515/// Build reverse index: mapping indices sorted by (source, original_line, original_column).
2516fn build_reverse_index(mappings: &[Mapping]) -> Vec<u32> {
2517    let mut indices: Vec<u32> = (0..mappings.len() as u32)
2518        .filter(|&i| mappings[i as usize].source != NO_SOURCE)
2519        .collect();
2520
2521    indices.sort_unstable_by(|&a, &b| {
2522        let ma = &mappings[a as usize];
2523        let mb = &mappings[b as usize];
2524        ma.source
2525            .cmp(&mb.source)
2526            .then(ma.original_line.cmp(&mb.original_line))
2527            .then(ma.original_column.cmp(&mb.original_column))
2528    });
2529
2530    indices
2531}
2532
2533// ── Streaming iterator ────────────────────────────────────────────
2534
2535/// Lazy iterator over VLQ-encoded source map mappings.
2536///
2537/// Decodes one mapping at a time without allocating a full `Vec<Mapping>`.
2538/// Useful for streaming composition pipelines where intermediate allocation
2539/// is undesirable.
2540///
2541/// # Examples
2542///
2543/// ```
2544/// use srcmap_sourcemap::MappingsIter;
2545///
2546/// let vlq = "AAAA;AACA,EAAA;AACA";
2547/// let mappings: Vec<_> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
2548/// assert_eq!(mappings.len(), 4);
2549/// assert_eq!(mappings[0].generated_line, 0);
2550/// assert_eq!(mappings[1].generated_line, 1);
2551/// ```
2552pub struct MappingsIter<'a> {
2553    bytes: &'a [u8],
2554    len: usize,
2555    pos: usize,
2556    source_index: i64,
2557    original_line: i64,
2558    original_column: i64,
2559    name_index: i64,
2560    generated_line: u32,
2561    generated_column: i64,
2562    done: bool,
2563}
2564
2565impl<'a> MappingsIter<'a> {
2566    /// Create a new iterator over VLQ-encoded mappings.
2567    pub fn new(vlq: &'a str) -> Self {
2568        let bytes = vlq.as_bytes();
2569        Self {
2570            bytes,
2571            len: bytes.len(),
2572            pos: 0,
2573            source_index: 0,
2574            original_line: 0,
2575            original_column: 0,
2576            name_index: 0,
2577            generated_line: 0,
2578            generated_column: 0,
2579            done: false,
2580        }
2581    }
2582}
2583
2584impl Iterator for MappingsIter<'_> {
2585    type Item = Result<Mapping, DecodeError>;
2586
2587    fn next(&mut self) -> Option<Self::Item> {
2588        if self.done {
2589            return None;
2590        }
2591
2592        loop {
2593            if self.pos >= self.len {
2594                self.done = true;
2595                return None;
2596            }
2597
2598            let byte = self.bytes[self.pos];
2599
2600            if byte == b';' {
2601                self.pos += 1;
2602                self.generated_line += 1;
2603                self.generated_column = 0;
2604                continue;
2605            }
2606
2607            if byte == b',' {
2608                self.pos += 1;
2609                continue;
2610            }
2611
2612            // Field 1: generated column
2613            match vlq_fast(self.bytes, &mut self.pos) {
2614                Ok(delta) => self.generated_column += delta,
2615                Err(e) => {
2616                    self.done = true;
2617                    return Some(Err(e));
2618                }
2619            }
2620
2621            if self.pos < self.len && self.bytes[self.pos] != b',' && self.bytes[self.pos] != b';' {
2622                // Fields 2-4: source, original line, original column
2623                match vlq_fast(self.bytes, &mut self.pos) {
2624                    Ok(delta) => self.source_index += delta,
2625                    Err(e) => {
2626                        self.done = true;
2627                        return Some(Err(e));
2628                    }
2629                }
2630                match vlq_fast(self.bytes, &mut self.pos) {
2631                    Ok(delta) => self.original_line += delta,
2632                    Err(e) => {
2633                        self.done = true;
2634                        return Some(Err(e));
2635                    }
2636                }
2637                match vlq_fast(self.bytes, &mut self.pos) {
2638                    Ok(delta) => self.original_column += delta,
2639                    Err(e) => {
2640                        self.done = true;
2641                        return Some(Err(e));
2642                    }
2643                }
2644
2645                // Field 5: name (optional)
2646                let name = if self.pos < self.len
2647                    && self.bytes[self.pos] != b','
2648                    && self.bytes[self.pos] != b';'
2649                {
2650                    match vlq_fast(self.bytes, &mut self.pos) {
2651                        Ok(delta) => {
2652                            self.name_index += delta;
2653                            self.name_index as u32
2654                        }
2655                        Err(e) => {
2656                            self.done = true;
2657                            return Some(Err(e));
2658                        }
2659                    }
2660                } else {
2661                    NO_NAME
2662                };
2663
2664                return Some(Ok(Mapping {
2665                    generated_line: self.generated_line,
2666                    generated_column: self.generated_column as u32,
2667                    source: self.source_index as u32,
2668                    original_line: self.original_line as u32,
2669                    original_column: self.original_column as u32,
2670                    name,
2671                    is_range_mapping: false,
2672                }));
2673            } else {
2674                // 1-field segment: no source info
2675                return Some(Ok(Mapping {
2676                    generated_line: self.generated_line,
2677                    generated_column: self.generated_column as u32,
2678                    source: NO_SOURCE,
2679                    original_line: 0,
2680                    original_column: 0,
2681                    name: NO_NAME,
2682                    is_range_mapping: false,
2683                }));
2684            }
2685        }
2686    }
2687}
2688
2689// ── Builder ────────────────────────────────────────────────────────
2690
2691/// Builder for incrementally constructing a [`SourceMap`] from iterators.
2692///
2693/// Avoids the need to pre-collect sources, names, and mappings into `Vec`s.
2694/// Delegates to [`SourceMap::from_parts`] internally.
2695pub struct SourceMapBuilder {
2696    file: Option<String>,
2697    source_root: Option<String>,
2698    sources: Vec<String>,
2699    sources_content: Vec<Option<String>>,
2700    names: Vec<String>,
2701    mappings: Vec<Mapping>,
2702    ignore_list: Vec<u32>,
2703    debug_id: Option<String>,
2704    scopes: Option<ScopeInfo>,
2705}
2706
2707impl SourceMapBuilder {
2708    pub fn new() -> Self {
2709        Self {
2710            file: None,
2711            source_root: None,
2712            sources: Vec::new(),
2713            sources_content: Vec::new(),
2714            names: Vec::new(),
2715            mappings: Vec::new(),
2716            ignore_list: Vec::new(),
2717            debug_id: None,
2718            scopes: None,
2719        }
2720    }
2721
2722    pub fn file(mut self, file: impl Into<String>) -> Self {
2723        self.file = Some(file.into());
2724        self
2725    }
2726
2727    pub fn source_root(mut self, root: impl Into<String>) -> Self {
2728        self.source_root = Some(root.into());
2729        self
2730    }
2731
2732    pub fn sources(mut self, sources: impl IntoIterator<Item = impl Into<String>>) -> Self {
2733        self.sources = sources.into_iter().map(Into::into).collect();
2734        self
2735    }
2736
2737    pub fn sources_content(
2738        mut self,
2739        content: impl IntoIterator<Item = Option<impl Into<String>>>,
2740    ) -> Self {
2741        self.sources_content = content.into_iter().map(|c| c.map(Into::into)).collect();
2742        self
2743    }
2744
2745    pub fn names(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
2746        self.names = names.into_iter().map(Into::into).collect();
2747        self
2748    }
2749
2750    pub fn mappings(mut self, mappings: impl IntoIterator<Item = Mapping>) -> Self {
2751        self.mappings = mappings.into_iter().collect();
2752        self
2753    }
2754
2755    pub fn ignore_list(mut self, list: impl IntoIterator<Item = u32>) -> Self {
2756        self.ignore_list = list.into_iter().collect();
2757        self
2758    }
2759
2760    pub fn debug_id(mut self, id: impl Into<String>) -> Self {
2761        self.debug_id = Some(id.into());
2762        self
2763    }
2764
2765    pub fn scopes(mut self, scopes: ScopeInfo) -> Self {
2766        self.scopes = Some(scopes);
2767        self
2768    }
2769
2770    /// Consume the builder and produce a [`SourceMap`].
2771    ///
2772    /// Mappings must be sorted by (generated_line, generated_column).
2773    pub fn build(self) -> SourceMap {
2774        SourceMap::from_parts(
2775            self.file,
2776            self.source_root,
2777            self.sources,
2778            self.sources_content,
2779            self.names,
2780            self.mappings,
2781            self.ignore_list,
2782            self.debug_id,
2783            self.scopes,
2784        )
2785    }
2786}
2787
2788impl Default for SourceMapBuilder {
2789    fn default() -> Self {
2790        Self::new()
2791    }
2792}
2793
2794// ── Tests ──────────────────────────────────────────────────────────
2795
2796#[cfg(test)]
2797mod tests {
2798    use super::*;
2799
2800    fn simple_map() -> &'static str {
2801        r#"{"version":3,"sources":["input.js"],"names":["hello"],"mappings":"AAAA;AACA,EAAA;AACA"}"#
2802    }
2803
2804    #[test]
2805    fn parse_basic() {
2806        let sm = SourceMap::from_json(simple_map()).unwrap();
2807        assert_eq!(sm.sources, vec!["input.js"]);
2808        assert_eq!(sm.names, vec!["hello"]);
2809        assert_eq!(sm.line_count(), 3);
2810        assert!(sm.mapping_count() > 0);
2811    }
2812
2813    #[test]
2814    fn to_json_roundtrip() {
2815        let json = simple_map();
2816        let sm = SourceMap::from_json(json).unwrap();
2817        let output = sm.to_json();
2818
2819        // Parse the output back and verify it produces identical lookups
2820        let sm2 = SourceMap::from_json(&output).unwrap();
2821        assert_eq!(sm2.sources, sm.sources);
2822        assert_eq!(sm2.names, sm.names);
2823        assert_eq!(sm2.mapping_count(), sm.mapping_count());
2824        assert_eq!(sm2.line_count(), sm.line_count());
2825
2826        // Verify all lookups match
2827        for m in sm.all_mappings() {
2828            let loc1 = sm.original_position_for(m.generated_line, m.generated_column);
2829            let loc2 = sm2.original_position_for(m.generated_line, m.generated_column);
2830            match (loc1, loc2) {
2831                (Some(a), Some(b)) => {
2832                    assert_eq!(a.source, b.source);
2833                    assert_eq!(a.line, b.line);
2834                    assert_eq!(a.column, b.column);
2835                    assert_eq!(a.name, b.name);
2836                }
2837                (None, None) => {}
2838                _ => panic!(
2839                    "lookup mismatch at ({}, {})",
2840                    m.generated_line, m.generated_column
2841                ),
2842            }
2843        }
2844    }
2845
2846    #[test]
2847    fn to_json_roundtrip_large() {
2848        let json = generate_test_sourcemap(50, 10, 3);
2849        let sm = SourceMap::from_json(&json).unwrap();
2850        let output = sm.to_json();
2851        let sm2 = SourceMap::from_json(&output).unwrap();
2852
2853        assert_eq!(sm2.mapping_count(), sm.mapping_count());
2854
2855        // Spot-check lookups
2856        for line in (0..sm.line_count() as u32).step_by(5) {
2857            for col in [0u32, 10, 20, 50] {
2858                let a = sm.original_position_for(line, col);
2859                let b = sm2.original_position_for(line, col);
2860                match (a, b) {
2861                    (Some(a), Some(b)) => {
2862                        assert_eq!(a.source, b.source);
2863                        assert_eq!(a.line, b.line);
2864                        assert_eq!(a.column, b.column);
2865                    }
2866                    (None, None) => {}
2867                    _ => panic!("mismatch at ({line}, {col})"),
2868                }
2869            }
2870        }
2871    }
2872
2873    #[test]
2874    fn to_json_preserves_fields() {
2875        let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["app.ts"],"sourcesContent":["const x = 1;"],"names":["x"],"mappings":"AAAAA","ignoreList":[0]}"#;
2876        let sm = SourceMap::from_json(json).unwrap();
2877        let output = sm.to_json();
2878
2879        assert!(output.contains(r#""file":"out.js""#));
2880        assert!(output.contains(r#""sourceRoot":"src/""#));
2881        assert!(output.contains(r#""sourcesContent":["const x = 1;"]"#));
2882        assert!(output.contains(r#""ignoreList":[0]"#));
2883
2884        // Note: sources will have sourceRoot prepended
2885        let sm2 = SourceMap::from_json(&output).unwrap();
2886        assert_eq!(sm2.file.as_deref(), Some("out.js"));
2887        assert_eq!(sm2.ignore_list, vec![0]);
2888    }
2889
2890    #[test]
2891    fn original_position_for_exact_match() {
2892        let sm = SourceMap::from_json(simple_map()).unwrap();
2893        let loc = sm.original_position_for(0, 0).unwrap();
2894        assert_eq!(loc.source, 0);
2895        assert_eq!(loc.line, 0);
2896        assert_eq!(loc.column, 0);
2897    }
2898
2899    #[test]
2900    fn original_position_for_column_within_segment() {
2901        let sm = SourceMap::from_json(simple_map()).unwrap();
2902        // Column 5 on line 1: should snap to the mapping at column 2
2903        let loc = sm.original_position_for(1, 5);
2904        assert!(loc.is_some());
2905    }
2906
2907    #[test]
2908    fn original_position_for_nonexistent_line() {
2909        let sm = SourceMap::from_json(simple_map()).unwrap();
2910        assert!(sm.original_position_for(999, 0).is_none());
2911    }
2912
2913    #[test]
2914    fn original_position_for_before_first_mapping() {
2915        // Line 1 first mapping is at column 2. Column 0 should return None.
2916        let sm = SourceMap::from_json(simple_map()).unwrap();
2917        let loc = sm.original_position_for(1, 0);
2918        // Column 0 on line 1: the first mapping at col 0 (AACA decodes to col=0, src delta=1...)
2919        // Actually depends on exact VLQ values. Let's just verify it doesn't crash.
2920        let _ = loc;
2921    }
2922
2923    #[test]
2924    fn generated_position_for_basic() {
2925        let sm = SourceMap::from_json(simple_map()).unwrap();
2926        let loc = sm.generated_position_for("input.js", 0, 0).unwrap();
2927        assert_eq!(loc.line, 0);
2928        assert_eq!(loc.column, 0);
2929    }
2930
2931    #[test]
2932    fn generated_position_for_unknown_source() {
2933        let sm = SourceMap::from_json(simple_map()).unwrap();
2934        assert!(sm.generated_position_for("nonexistent.js", 0, 0).is_none());
2935    }
2936
2937    #[test]
2938    fn parse_invalid_version() {
2939        let json = r#"{"version":2,"sources":[],"names":[],"mappings":""}"#;
2940        let err = SourceMap::from_json(json).unwrap_err();
2941        assert!(matches!(err, ParseError::InvalidVersion(2)));
2942    }
2943
2944    #[test]
2945    fn parse_empty_mappings() {
2946        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
2947        let sm = SourceMap::from_json(json).unwrap();
2948        assert_eq!(sm.mapping_count(), 0);
2949        assert!(sm.original_position_for(0, 0).is_none());
2950    }
2951
2952    #[test]
2953    fn parse_with_source_root() {
2954        let json = r#"{"version":3,"sourceRoot":"src/","sources":["foo.js"],"names":[],"mappings":"AAAA"}"#;
2955        let sm = SourceMap::from_json(json).unwrap();
2956        assert_eq!(sm.sources, vec!["src/foo.js"]);
2957    }
2958
2959    #[test]
2960    fn parse_with_sources_content() {
2961        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var x = 1;"],"names":[],"mappings":"AAAA"}"#;
2962        let sm = SourceMap::from_json(json).unwrap();
2963        assert_eq!(sm.sources_content, vec![Some("var x = 1;".to_string())]);
2964    }
2965
2966    #[test]
2967    fn mappings_for_line() {
2968        let sm = SourceMap::from_json(simple_map()).unwrap();
2969        let line0 = sm.mappings_for_line(0);
2970        assert!(!line0.is_empty());
2971        let empty = sm.mappings_for_line(999);
2972        assert!(empty.is_empty());
2973    }
2974
2975    #[test]
2976    fn large_sourcemap_lookup() {
2977        // Generate a realistic source map
2978        let json = generate_test_sourcemap(500, 20, 5);
2979        let sm = SourceMap::from_json(&json).unwrap();
2980
2981        // Verify lookups work across the whole map
2982        for line in [0, 10, 100, 250, 499] {
2983            let mappings = sm.mappings_for_line(line);
2984            if let Some(m) = mappings.first() {
2985                let loc = sm.original_position_for(line, m.generated_column);
2986                assert!(loc.is_some(), "lookup failed for line {line}");
2987            }
2988        }
2989    }
2990
2991    #[test]
2992    fn reverse_lookup_roundtrip() {
2993        let json = generate_test_sourcemap(100, 10, 3);
2994        let sm = SourceMap::from_json(&json).unwrap();
2995
2996        // Pick a mapping and verify forward + reverse roundtrip
2997        let mapping = &sm.mappings[50];
2998        if mapping.source != NO_SOURCE {
2999            let source_name = sm.source(mapping.source);
3000            let result = sm.generated_position_for(
3001                source_name,
3002                mapping.original_line,
3003                mapping.original_column,
3004            );
3005            assert!(result.is_some(), "reverse lookup failed");
3006        }
3007    }
3008
3009    #[test]
3010    fn all_generated_positions_for_basic() {
3011        let sm = SourceMap::from_json(simple_map()).unwrap();
3012        let results = sm.all_generated_positions_for("input.js", 0, 0);
3013        assert!(!results.is_empty(), "should find at least one position");
3014        assert_eq!(results[0].line, 0);
3015        assert_eq!(results[0].column, 0);
3016    }
3017
3018    #[test]
3019    fn all_generated_positions_for_unknown_source() {
3020        let sm = SourceMap::from_json(simple_map()).unwrap();
3021        let results = sm.all_generated_positions_for("nonexistent.js", 0, 0);
3022        assert!(results.is_empty());
3023    }
3024
3025    #[test]
3026    fn all_generated_positions_for_no_match() {
3027        let sm = SourceMap::from_json(simple_map()).unwrap();
3028        let results = sm.all_generated_positions_for("input.js", 999, 999);
3029        assert!(results.is_empty());
3030    }
3031
3032    #[test]
3033    fn encode_mappings_roundtrip() {
3034        let json = generate_test_sourcemap(50, 10, 3);
3035        let sm = SourceMap::from_json(&json).unwrap();
3036        let encoded = sm.encode_mappings();
3037        // Re-parse with encoded mappings
3038        let json2 = format!(
3039            r#"{{"version":3,"sources":{sources},"names":{names},"mappings":"{mappings}"}}"#,
3040            sources = serde_json::to_string(&sm.sources).unwrap(),
3041            names = serde_json::to_string(&sm.names).unwrap(),
3042            mappings = encoded,
3043        );
3044        let sm2 = SourceMap::from_json(&json2).unwrap();
3045        assert_eq!(sm2.mapping_count(), sm.mapping_count());
3046    }
3047
3048    #[test]
3049    fn indexed_source_map() {
3050        let json = r#"{
3051            "version": 3,
3052            "file": "bundle.js",
3053            "sections": [
3054                {
3055                    "offset": {"line": 0, "column": 0},
3056                    "map": {
3057                        "version": 3,
3058                        "sources": ["a.js"],
3059                        "names": ["foo"],
3060                        "mappings": "AAAAA"
3061                    }
3062                },
3063                {
3064                    "offset": {"line": 10, "column": 0},
3065                    "map": {
3066                        "version": 3,
3067                        "sources": ["b.js"],
3068                        "names": ["bar"],
3069                        "mappings": "AAAAA"
3070                    }
3071                }
3072            ]
3073        }"#;
3074
3075        let sm = SourceMap::from_json(json).unwrap();
3076
3077        // Should have both sources
3078        assert_eq!(sm.sources.len(), 2);
3079        assert!(sm.sources.contains(&"a.js".to_string()));
3080        assert!(sm.sources.contains(&"b.js".to_string()));
3081
3082        // Should have both names
3083        assert_eq!(sm.names.len(), 2);
3084        assert!(sm.names.contains(&"foo".to_string()));
3085        assert!(sm.names.contains(&"bar".to_string()));
3086
3087        // First section: line 0, col 0 should map to a.js
3088        let loc = sm.original_position_for(0, 0).unwrap();
3089        assert_eq!(sm.source(loc.source), "a.js");
3090        assert_eq!(loc.line, 0);
3091        assert_eq!(loc.column, 0);
3092
3093        // Second section: line 10, col 0 should map to b.js
3094        let loc = sm.original_position_for(10, 0).unwrap();
3095        assert_eq!(sm.source(loc.source), "b.js");
3096        assert_eq!(loc.line, 0);
3097        assert_eq!(loc.column, 0);
3098    }
3099
3100    #[test]
3101    fn indexed_source_map_shared_sources() {
3102        // Two sections referencing the same source
3103        let json = r#"{
3104            "version": 3,
3105            "sections": [
3106                {
3107                    "offset": {"line": 0, "column": 0},
3108                    "map": {
3109                        "version": 3,
3110                        "sources": ["shared.js"],
3111                        "names": [],
3112                        "mappings": "AAAA"
3113                    }
3114                },
3115                {
3116                    "offset": {"line": 5, "column": 0},
3117                    "map": {
3118                        "version": 3,
3119                        "sources": ["shared.js"],
3120                        "names": [],
3121                        "mappings": "AACA"
3122                    }
3123                }
3124            ]
3125        }"#;
3126
3127        let sm = SourceMap::from_json(json).unwrap();
3128
3129        // Should deduplicate sources
3130        assert_eq!(sm.sources.len(), 1);
3131        assert_eq!(sm.sources[0], "shared.js");
3132
3133        // Both sections should resolve to the same source
3134        let loc0 = sm.original_position_for(0, 0).unwrap();
3135        let loc5 = sm.original_position_for(5, 0).unwrap();
3136        assert_eq!(loc0.source, loc5.source);
3137    }
3138
3139    #[test]
3140    fn parse_ignore_list() {
3141        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
3142        let sm = SourceMap::from_json(json).unwrap();
3143        assert_eq!(sm.ignore_list, vec![1]);
3144    }
3145
3146    /// Helper: build a source map JSON from absolute mappings data.
3147    fn build_sourcemap_json(
3148        sources: &[&str],
3149        names: &[&str],
3150        mappings_data: &[Vec<Vec<i64>>],
3151    ) -> String {
3152        let converted: Vec<Vec<srcmap_codec::Segment>> = mappings_data
3153            .iter()
3154            .map(|line| {
3155                line.iter()
3156                    .map(|seg| srcmap_codec::Segment::from(seg.as_slice()))
3157                    .collect()
3158            })
3159            .collect();
3160        let encoded = srcmap_codec::encode(&converted);
3161        format!(
3162            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
3163            sources
3164                .iter()
3165                .map(|s| format!("\"{s}\""))
3166                .collect::<Vec<_>>()
3167                .join(","),
3168            names
3169                .iter()
3170                .map(|n| format!("\"{n}\""))
3171                .collect::<Vec<_>>()
3172                .join(","),
3173            encoded,
3174        )
3175    }
3176
3177    // ── 1. Edge cases in decode_mappings ────────────────────────────
3178
3179    #[test]
3180    fn decode_multiple_consecutive_semicolons() {
3181        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
3182        let sm = SourceMap::from_json(json).unwrap();
3183        assert_eq!(sm.line_count(), 4);
3184        assert!(sm.mappings_for_line(1).is_empty());
3185        assert!(sm.mappings_for_line(2).is_empty());
3186        assert!(!sm.mappings_for_line(0).is_empty());
3187        assert!(!sm.mappings_for_line(3).is_empty());
3188    }
3189
3190    #[test]
3191    fn decode_trailing_semicolons() {
3192        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;"}"#;
3193        let sm = SourceMap::from_json(json).unwrap();
3194        assert_eq!(sm.line_count(), 3);
3195        assert!(!sm.mappings_for_line(0).is_empty());
3196        assert!(sm.mappings_for_line(1).is_empty());
3197        assert!(sm.mappings_for_line(2).is_empty());
3198    }
3199
3200    #[test]
3201    fn decode_leading_comma() {
3202        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":",AAAA"}"#;
3203        let sm = SourceMap::from_json(json).unwrap();
3204        assert_eq!(sm.mapping_count(), 1);
3205        let m = &sm.all_mappings()[0];
3206        assert_eq!(m.generated_line, 0);
3207        assert_eq!(m.generated_column, 0);
3208    }
3209
3210    #[test]
3211    fn decode_single_field_segments() {
3212        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,C"}"#;
3213        let sm = SourceMap::from_json(json).unwrap();
3214        assert_eq!(sm.mapping_count(), 2);
3215        for m in sm.all_mappings() {
3216            assert_eq!(m.source, NO_SOURCE);
3217        }
3218        assert_eq!(sm.all_mappings()[0].generated_column, 0);
3219        assert_eq!(sm.all_mappings()[1].generated_column, 1);
3220        assert!(sm.original_position_for(0, 0).is_none());
3221        assert!(sm.original_position_for(0, 1).is_none());
3222    }
3223
3224    #[test]
3225    fn decode_five_field_segments_with_names() {
3226        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0, 0], vec![10, 0, 0, 5, 1]]];
3227        let json = build_sourcemap_json(&["app.js"], &["foo", "bar"], &mappings_data);
3228        let sm = SourceMap::from_json(&json).unwrap();
3229        assert_eq!(sm.mapping_count(), 2);
3230        assert_eq!(sm.all_mappings()[0].name, 0);
3231        assert_eq!(sm.all_mappings()[1].name, 1);
3232
3233        let loc = sm.original_position_for(0, 0).unwrap();
3234        assert_eq!(loc.name, Some(0));
3235        assert_eq!(sm.name(0), "foo");
3236
3237        let loc = sm.original_position_for(0, 10).unwrap();
3238        assert_eq!(loc.name, Some(1));
3239        assert_eq!(sm.name(1), "bar");
3240    }
3241
3242    #[test]
3243    fn decode_large_vlq_values() {
3244        let mappings_data = vec![vec![vec![500_i64, 0, 1000, 2000]]];
3245        let json = build_sourcemap_json(&["big.js"], &[], &mappings_data);
3246        let sm = SourceMap::from_json(&json).unwrap();
3247        assert_eq!(sm.mapping_count(), 1);
3248        let m = &sm.all_mappings()[0];
3249        assert_eq!(m.generated_column, 500);
3250        assert_eq!(m.original_line, 1000);
3251        assert_eq!(m.original_column, 2000);
3252
3253        let loc = sm.original_position_for(0, 500).unwrap();
3254        assert_eq!(loc.line, 1000);
3255        assert_eq!(loc.column, 2000);
3256    }
3257
3258    #[test]
3259    fn decode_only_semicolons() {
3260        let json = r#"{"version":3,"sources":[],"names":[],"mappings":";;;"}"#;
3261        let sm = SourceMap::from_json(json).unwrap();
3262        assert_eq!(sm.line_count(), 4);
3263        assert_eq!(sm.mapping_count(), 0);
3264        for line in 0..4 {
3265            assert!(sm.mappings_for_line(line).is_empty());
3266        }
3267    }
3268
3269    #[test]
3270    fn decode_mixed_single_and_four_field_segments() {
3271        let mappings_data = vec![vec![srcmap_codec::Segment::four(5, 0, 0, 0)]];
3272        let four_field_encoded = srcmap_codec::encode(&mappings_data);
3273        let combined_mappings = format!("A,{four_field_encoded}");
3274        let json = format!(
3275            r#"{{"version":3,"sources":["x.js"],"names":[],"mappings":"{combined_mappings}"}}"#,
3276        );
3277        let sm = SourceMap::from_json(&json).unwrap();
3278        assert_eq!(sm.mapping_count(), 2);
3279        assert_eq!(sm.all_mappings()[0].source, NO_SOURCE);
3280        assert_eq!(sm.all_mappings()[1].source, 0);
3281    }
3282
3283    // ── 2. Source map parsing ───────────────────────────────────────
3284
3285    #[test]
3286    fn parse_missing_optional_fields() {
3287        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3288        let sm = SourceMap::from_json(json).unwrap();
3289        assert!(sm.file.is_none());
3290        assert!(sm.source_root.is_none());
3291        assert!(sm.sources_content.is_empty());
3292        assert!(sm.ignore_list.is_empty());
3293    }
3294
3295    #[test]
3296    fn parse_with_file_field() {
3297        let json =
3298            r#"{"version":3,"file":"output.js","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3299        let sm = SourceMap::from_json(json).unwrap();
3300        assert_eq!(sm.file.as_deref(), Some("output.js"));
3301    }
3302
3303    #[test]
3304    fn parse_null_entries_in_sources() {
3305        let json = r#"{"version":3,"sources":["a.js",null,"c.js"],"names":[],"mappings":"AAAA"}"#;
3306        let sm = SourceMap::from_json(json).unwrap();
3307        assert_eq!(sm.sources.len(), 3);
3308        assert_eq!(sm.sources[0], "a.js");
3309        assert_eq!(sm.sources[1], "");
3310        assert_eq!(sm.sources[2], "c.js");
3311    }
3312
3313    #[test]
3314    fn parse_null_entries_in_sources_with_source_root() {
3315        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js",null],"names":[],"mappings":"AAAA"}"#;
3316        let sm = SourceMap::from_json(json).unwrap();
3317        assert_eq!(sm.sources[0], "lib/a.js");
3318        assert_eq!(sm.sources[1], "");
3319    }
3320
3321    #[test]
3322    fn parse_empty_names_array() {
3323        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3324        let sm = SourceMap::from_json(json).unwrap();
3325        assert!(sm.names.is_empty());
3326    }
3327
3328    #[test]
3329    fn parse_invalid_json() {
3330        let result = SourceMap::from_json("not valid json");
3331        assert!(result.is_err());
3332        assert!(matches!(result.unwrap_err(), ParseError::Json(_)));
3333    }
3334
3335    #[test]
3336    fn parse_json_missing_version() {
3337        let result = SourceMap::from_json(r#"{"sources":[],"names":[],"mappings":""}"#);
3338        assert!(result.is_err());
3339    }
3340
3341    #[test]
3342    fn parse_multiple_sources_overlapping_original_positions() {
3343        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10], vec![10, 1, 5, 10]]];
3344        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
3345        let sm = SourceMap::from_json(&json).unwrap();
3346
3347        let loc0 = sm.original_position_for(0, 0).unwrap();
3348        assert_eq!(loc0.source, 0);
3349        assert_eq!(sm.source(loc0.source), "a.js");
3350
3351        let loc1 = sm.original_position_for(0, 10).unwrap();
3352        assert_eq!(loc1.source, 1);
3353        assert_eq!(sm.source(loc1.source), "b.js");
3354
3355        assert_eq!(loc0.line, loc1.line);
3356        assert_eq!(loc0.column, loc1.column);
3357    }
3358
3359    #[test]
3360    fn parse_sources_content_with_null_entries() {
3361        let json = r#"{"version":3,"sources":["a.js","b.js"],"sourcesContent":["content a",null],"names":[],"mappings":"AAAA"}"#;
3362        let sm = SourceMap::from_json(json).unwrap();
3363        assert_eq!(sm.sources_content.len(), 2);
3364        assert_eq!(sm.sources_content[0], Some("content a".to_string()));
3365        assert_eq!(sm.sources_content[1], None);
3366    }
3367
3368    #[test]
3369    fn parse_empty_sources_and_names() {
3370        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
3371        let sm = SourceMap::from_json(json).unwrap();
3372        assert!(sm.sources.is_empty());
3373        assert!(sm.names.is_empty());
3374        assert_eq!(sm.mapping_count(), 0);
3375    }
3376
3377    // ── 3. Position lookups ─────────────────────────────────────────
3378
3379    #[test]
3380    fn lookup_exact_match() {
3381        let mappings_data = vec![vec![
3382            vec![0_i64, 0, 10, 20],
3383            vec![5, 0, 10, 25],
3384            vec![15, 0, 11, 0],
3385        ]];
3386        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3387        let sm = SourceMap::from_json(&json).unwrap();
3388
3389        let loc = sm.original_position_for(0, 5).unwrap();
3390        assert_eq!(loc.line, 10);
3391        assert_eq!(loc.column, 25);
3392    }
3393
3394    #[test]
3395    fn lookup_before_first_segment() {
3396        let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
3397        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3398        let sm = SourceMap::from_json(&json).unwrap();
3399
3400        assert!(sm.original_position_for(0, 0).is_none());
3401        assert!(sm.original_position_for(0, 4).is_none());
3402    }
3403
3404    #[test]
3405    fn lookup_between_segments() {
3406        let mappings_data = vec![vec![
3407            vec![0_i64, 0, 1, 0],
3408            vec![10, 0, 2, 0],
3409            vec![20, 0, 3, 0],
3410        ]];
3411        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3412        let sm = SourceMap::from_json(&json).unwrap();
3413
3414        let loc = sm.original_position_for(0, 7).unwrap();
3415        assert_eq!(loc.line, 1);
3416        assert_eq!(loc.column, 0);
3417
3418        let loc = sm.original_position_for(0, 15).unwrap();
3419        assert_eq!(loc.line, 2);
3420        assert_eq!(loc.column, 0);
3421    }
3422
3423    #[test]
3424    fn lookup_after_last_segment() {
3425        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 1, 5]]];
3426        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3427        let sm = SourceMap::from_json(&json).unwrap();
3428
3429        let loc = sm.original_position_for(0, 100).unwrap();
3430        assert_eq!(loc.line, 1);
3431        assert_eq!(loc.column, 5);
3432    }
3433
3434    #[test]
3435    fn lookup_empty_lines_no_mappings() {
3436        let mappings_data = vec![
3437            vec![vec![0_i64, 0, 0, 0]],
3438            vec![],
3439            vec![vec![0_i64, 0, 2, 0]],
3440        ];
3441        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3442        let sm = SourceMap::from_json(&json).unwrap();
3443
3444        assert!(sm.original_position_for(1, 0).is_none());
3445        assert!(sm.original_position_for(1, 10).is_none());
3446        assert!(sm.original_position_for(0, 0).is_some());
3447        assert!(sm.original_position_for(2, 0).is_some());
3448    }
3449
3450    #[test]
3451    fn lookup_line_with_single_mapping() {
3452        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
3453        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3454        let sm = SourceMap::from_json(&json).unwrap();
3455
3456        let loc = sm.original_position_for(0, 0).unwrap();
3457        assert_eq!(loc.line, 0);
3458        assert_eq!(loc.column, 0);
3459
3460        let loc = sm.original_position_for(0, 50).unwrap();
3461        assert_eq!(loc.line, 0);
3462        assert_eq!(loc.column, 0);
3463    }
3464
3465    #[test]
3466    fn lookup_column_0_vs_column_nonzero() {
3467        let mappings_data = vec![vec![vec![0_i64, 0, 10, 0], vec![8, 0, 20, 5]]];
3468        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3469        let sm = SourceMap::from_json(&json).unwrap();
3470
3471        let loc0 = sm.original_position_for(0, 0).unwrap();
3472        assert_eq!(loc0.line, 10);
3473        assert_eq!(loc0.column, 0);
3474
3475        let loc8 = sm.original_position_for(0, 8).unwrap();
3476        assert_eq!(loc8.line, 20);
3477        assert_eq!(loc8.column, 5);
3478
3479        let loc4 = sm.original_position_for(0, 4).unwrap();
3480        assert_eq!(loc4.line, 10);
3481    }
3482
3483    #[test]
3484    fn lookup_beyond_last_line() {
3485        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
3486        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3487        let sm = SourceMap::from_json(&json).unwrap();
3488
3489        assert!(sm.original_position_for(1, 0).is_none());
3490        assert!(sm.original_position_for(100, 0).is_none());
3491    }
3492
3493    #[test]
3494    fn lookup_single_field_returns_none() {
3495        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A"}"#;
3496        let sm = SourceMap::from_json(json).unwrap();
3497        assert_eq!(sm.mapping_count(), 1);
3498        assert!(sm.original_position_for(0, 0).is_none());
3499    }
3500
3501    // ── 4. Reverse lookups (generated_position_for) ─────────────────
3502
3503    #[test]
3504    fn reverse_lookup_exact_match() {
3505        let mappings_data = vec![
3506            vec![vec![0_i64, 0, 0, 0]],
3507            vec![vec![4, 0, 1, 0], vec![10, 0, 1, 8]],
3508            vec![vec![0, 0, 2, 0]],
3509        ];
3510        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
3511        let sm = SourceMap::from_json(&json).unwrap();
3512
3513        let loc = sm.generated_position_for("main.js", 1, 8).unwrap();
3514        assert_eq!(loc.line, 1);
3515        assert_eq!(loc.column, 10);
3516    }
3517
3518    #[test]
3519    fn reverse_lookup_no_match() {
3520        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10]]];
3521        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
3522        let sm = SourceMap::from_json(&json).unwrap();
3523
3524        assert!(sm.generated_position_for("main.js", 99, 0).is_none());
3525    }
3526
3527    #[test]
3528    fn reverse_lookup_unknown_source() {
3529        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
3530        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
3531        let sm = SourceMap::from_json(&json).unwrap();
3532
3533        assert!(sm.generated_position_for("unknown.js", 0, 0).is_none());
3534    }
3535
3536    #[test]
3537    fn reverse_lookup_multiple_mappings_same_original() {
3538        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]], vec![vec![20, 0, 5, 10]]];
3539        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3540        let sm = SourceMap::from_json(&json).unwrap();
3541
3542        let loc = sm.generated_position_for("src.js", 5, 10);
3543        assert!(loc.is_some());
3544        let loc = loc.unwrap();
3545        assert!(
3546            (loc.line == 0 && loc.column == 0) || (loc.line == 1 && loc.column == 20),
3547            "Expected (0,0) or (1,20), got ({},{})",
3548            loc.line,
3549            loc.column
3550        );
3551    }
3552
3553    #[test]
3554    fn reverse_lookup_with_multiple_sources() {
3555        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 1, 0, 0]]];
3556        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
3557        let sm = SourceMap::from_json(&json).unwrap();
3558
3559        let loc_a = sm.generated_position_for("a.js", 0, 0).unwrap();
3560        assert_eq!(loc_a.line, 0);
3561        assert_eq!(loc_a.column, 0);
3562
3563        let loc_b = sm.generated_position_for("b.js", 0, 0).unwrap();
3564        assert_eq!(loc_b.line, 0);
3565        assert_eq!(loc_b.column, 10);
3566    }
3567
3568    #[test]
3569    fn reverse_lookup_skips_single_field_segments() {
3570        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
3571        let sm = SourceMap::from_json(json).unwrap();
3572
3573        let loc = sm.generated_position_for("a.js", 0, 0).unwrap();
3574        assert_eq!(loc.line, 0);
3575        assert_eq!(loc.column, 5);
3576    }
3577
3578    #[test]
3579    fn reverse_lookup_finds_each_original_line() {
3580        let mappings_data = vec![
3581            vec![vec![0_i64, 0, 0, 0]],
3582            vec![vec![0, 0, 1, 0]],
3583            vec![vec![0, 0, 2, 0]],
3584            vec![vec![0, 0, 3, 0]],
3585        ];
3586        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
3587        let sm = SourceMap::from_json(&json).unwrap();
3588
3589        for orig_line in 0..4 {
3590            let loc = sm.generated_position_for("x.js", orig_line, 0).unwrap();
3591            assert_eq!(
3592                loc.line, orig_line,
3593                "reverse lookup for orig line {orig_line}"
3594            );
3595            assert_eq!(loc.column, 0);
3596        }
3597    }
3598
3599    // ── 5. ignoreList ───────────────────────────────────────────────
3600
3601    #[test]
3602    fn parse_with_ignore_list_multiple() {
3603        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js","vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[1,2]}"#;
3604        let sm = SourceMap::from_json(json).unwrap();
3605        assert_eq!(sm.ignore_list, vec![1, 2]);
3606    }
3607
3608    #[test]
3609    fn parse_with_empty_ignore_list() {
3610        let json =
3611            r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA","ignoreList":[]}"#;
3612        let sm = SourceMap::from_json(json).unwrap();
3613        assert!(sm.ignore_list.is_empty());
3614    }
3615
3616    #[test]
3617    fn parse_without_ignore_list_field() {
3618        let json = r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA"}"#;
3619        let sm = SourceMap::from_json(json).unwrap();
3620        assert!(sm.ignore_list.is_empty());
3621    }
3622
3623    // ── Additional edge case tests ──────────────────────────────────
3624
3625    #[test]
3626    fn source_index_lookup() {
3627        let json = r#"{"version":3,"sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA"}"#;
3628        let sm = SourceMap::from_json(json).unwrap();
3629        assert_eq!(sm.source_index("a.js"), Some(0));
3630        assert_eq!(sm.source_index("b.js"), Some(1));
3631        assert_eq!(sm.source_index("c.js"), Some(2));
3632        assert_eq!(sm.source_index("d.js"), None);
3633    }
3634
3635    #[test]
3636    fn all_mappings_returns_complete_list() {
3637        let mappings_data = vec![
3638            vec![vec![0_i64, 0, 0, 0], vec![5, 0, 0, 5]],
3639            vec![vec![0, 0, 1, 0]],
3640        ];
3641        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
3642        let sm = SourceMap::from_json(&json).unwrap();
3643        assert_eq!(sm.all_mappings().len(), 3);
3644        assert_eq!(sm.mapping_count(), 3);
3645    }
3646
3647    #[test]
3648    fn line_count_matches_decoded_lines() {
3649        let mappings_data = vec![
3650            vec![vec![0_i64, 0, 0, 0]],
3651            vec![],
3652            vec![vec![0_i64, 0, 2, 0]],
3653            vec![],
3654            vec![],
3655        ];
3656        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
3657        let sm = SourceMap::from_json(&json).unwrap();
3658        assert_eq!(sm.line_count(), 5);
3659    }
3660
3661    #[test]
3662    fn parse_error_display() {
3663        let err = ParseError::InvalidVersion(5);
3664        assert_eq!(format!("{err}"), "unsupported source map version: 5");
3665
3666        let json_err = SourceMap::from_json("{}").unwrap_err();
3667        let display = format!("{json_err}");
3668        assert!(display.contains("JSON parse error") || display.contains("missing field"));
3669    }
3670
3671    #[test]
3672    fn original_position_name_none_for_four_field() {
3673        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]]];
3674        let json = build_sourcemap_json(&["a.js"], &["unused_name"], &mappings_data);
3675        let sm = SourceMap::from_json(&json).unwrap();
3676
3677        let loc = sm.original_position_for(0, 0).unwrap();
3678        assert!(loc.name.is_none());
3679    }
3680
3681    #[test]
3682    fn forward_and_reverse_roundtrip_comprehensive() {
3683        let mappings_data = vec![
3684            vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10], vec![20, 1, 5, 0]],
3685            vec![vec![0, 0, 1, 0], vec![5, 1, 6, 3]],
3686            vec![vec![0, 0, 2, 0]],
3687        ];
3688        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
3689        let sm = SourceMap::from_json(&json).unwrap();
3690
3691        for m in sm.all_mappings() {
3692            if m.source == NO_SOURCE {
3693                continue;
3694            }
3695            let source_name = sm.source(m.source);
3696
3697            let orig = sm
3698                .original_position_for(m.generated_line, m.generated_column)
3699                .unwrap();
3700            assert_eq!(orig.source, m.source);
3701            assert_eq!(orig.line, m.original_line);
3702            assert_eq!(orig.column, m.original_column);
3703
3704            let gen_loc = sm
3705                .generated_position_for(source_name, m.original_line, m.original_column)
3706                .unwrap();
3707            assert_eq!(gen_loc.line, m.generated_line);
3708            assert_eq!(gen_loc.column, m.generated_column);
3709        }
3710    }
3711
3712    // ── 6. Comprehensive edge case tests ────────────────────────────
3713
3714    // -- sourceRoot edge cases --
3715
3716    #[test]
3717    fn source_root_with_multiple_sources() {
3718        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA,KACA,KACA"}"#;
3719        let sm = SourceMap::from_json(json).unwrap();
3720        assert_eq!(sm.sources, vec!["lib/a.js", "lib/b.js", "lib/c.js"]);
3721    }
3722
3723    #[test]
3724    fn source_root_empty_string() {
3725        let json =
3726            r#"{"version":3,"sourceRoot":"","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3727        let sm = SourceMap::from_json(json).unwrap();
3728        assert_eq!(sm.sources, vec!["a.js"]);
3729    }
3730
3731    #[test]
3732    fn source_root_preserved_in_to_json() {
3733        let json =
3734            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3735        let sm = SourceMap::from_json(json).unwrap();
3736        let output = sm.to_json();
3737        assert!(output.contains(r#""sourceRoot":"src/""#));
3738    }
3739
3740    #[test]
3741    fn source_root_reverse_lookup_uses_prefixed_name() {
3742        let json =
3743            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3744        let sm = SourceMap::from_json(json).unwrap();
3745        // Must use the prefixed name for reverse lookups
3746        assert!(sm.generated_position_for("src/a.js", 0, 0).is_some());
3747        assert!(sm.generated_position_for("a.js", 0, 0).is_none());
3748    }
3749
3750    #[test]
3751    fn source_root_with_trailing_slash() {
3752        let json =
3753            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3754        let sm = SourceMap::from_json(json).unwrap();
3755        assert_eq!(sm.sources[0], "src/a.js");
3756    }
3757
3758    #[test]
3759    fn source_root_without_trailing_slash() {
3760        let json =
3761            r#"{"version":3,"sourceRoot":"src","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3762        let sm = SourceMap::from_json(json).unwrap();
3763        // sourceRoot is applied as raw prefix during parsing
3764        assert_eq!(sm.sources[0], "srca.js");
3765        // Roundtrip should strip the prefix back correctly
3766        let output = sm.to_json();
3767        let sm2 = SourceMap::from_json(&output).unwrap();
3768        assert_eq!(sm2.sources[0], "srca.js");
3769    }
3770
3771    // -- JSON/parsing error cases --
3772
3773    #[test]
3774    fn parse_empty_json_object() {
3775        // {} has no version field
3776        let result = SourceMap::from_json("{}");
3777        assert!(result.is_err());
3778    }
3779
3780    #[test]
3781    fn parse_version_0() {
3782        let json = r#"{"version":0,"sources":[],"names":[],"mappings":""}"#;
3783        assert!(matches!(
3784            SourceMap::from_json(json).unwrap_err(),
3785            ParseError::InvalidVersion(0)
3786        ));
3787    }
3788
3789    #[test]
3790    fn parse_version_4() {
3791        let json = r#"{"version":4,"sources":[],"names":[],"mappings":""}"#;
3792        assert!(matches!(
3793            SourceMap::from_json(json).unwrap_err(),
3794            ParseError::InvalidVersion(4)
3795        ));
3796    }
3797
3798    #[test]
3799    fn parse_extra_unknown_fields_ignored() {
3800        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom_field":true,"x_debug":{"foo":"bar"}}"#;
3801        let sm = SourceMap::from_json(json).unwrap();
3802        assert_eq!(sm.mapping_count(), 1);
3803    }
3804
3805    #[test]
3806    fn parse_vlq_error_propagated() {
3807        // '!' is not valid base64 — should surface as VLQ error
3808        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AA!A"}"#;
3809        let result = SourceMap::from_json(json);
3810        assert!(result.is_err());
3811        assert!(matches!(result.unwrap_err(), ParseError::Vlq(_)));
3812    }
3813
3814    #[test]
3815    fn parse_truncated_vlq_error() {
3816        // 'g' has continuation bit set — truncated VLQ
3817        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"g"}"#;
3818        let result = SourceMap::from_json(json);
3819        assert!(result.is_err());
3820    }
3821
3822    // -- to_json edge cases --
3823
3824    #[test]
3825    fn to_json_produces_valid_json() {
3826        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]}"#;
3827        let sm = SourceMap::from_json(json).unwrap();
3828        let output = sm.to_json();
3829        // Must be valid JSON that serde can parse
3830        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
3831    }
3832
3833    #[test]
3834    fn to_json_escapes_special_chars() {
3835        let json = r#"{"version":3,"sources":["path/with\"quotes.js"],"sourcesContent":["line1\nline2\ttab\\backslash"],"names":[],"mappings":"AAAA"}"#;
3836        let sm = SourceMap::from_json(json).unwrap();
3837        let output = sm.to_json();
3838        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
3839        let sm2 = SourceMap::from_json(&output).unwrap();
3840        assert_eq!(
3841            sm2.sources_content[0].as_deref(),
3842            Some("line1\nline2\ttab\\backslash")
3843        );
3844    }
3845
3846    #[test]
3847    fn to_json_empty_map() {
3848        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
3849        let sm = SourceMap::from_json(json).unwrap();
3850        let output = sm.to_json();
3851        let sm2 = SourceMap::from_json(&output).unwrap();
3852        assert_eq!(sm2.mapping_count(), 0);
3853        assert!(sm2.sources.is_empty());
3854    }
3855
3856    #[test]
3857    fn to_json_roundtrip_with_names() {
3858        let mappings_data = vec![vec![
3859            vec![0_i64, 0, 0, 0, 0],
3860            vec![10, 0, 0, 10, 1],
3861            vec![20, 0, 1, 0, 2],
3862        ]];
3863        let json = build_sourcemap_json(&["src.js"], &["foo", "bar", "baz"], &mappings_data);
3864        let sm = SourceMap::from_json(&json).unwrap();
3865        let output = sm.to_json();
3866        let sm2 = SourceMap::from_json(&output).unwrap();
3867
3868        for m in sm2.all_mappings() {
3869            if m.source != NO_SOURCE && m.name != NO_NAME {
3870                let loc = sm2
3871                    .original_position_for(m.generated_line, m.generated_column)
3872                    .unwrap();
3873                assert!(loc.name.is_some());
3874            }
3875        }
3876    }
3877
3878    // -- Indexed source map edge cases --
3879
3880    #[test]
3881    fn indexed_source_map_column_offset() {
3882        let json = r#"{
3883            "version": 3,
3884            "sections": [
3885                {
3886                    "offset": {"line": 0, "column": 10},
3887                    "map": {
3888                        "version": 3,
3889                        "sources": ["a.js"],
3890                        "names": [],
3891                        "mappings": "AAAA"
3892                    }
3893                }
3894            ]
3895        }"#;
3896        let sm = SourceMap::from_json(json).unwrap();
3897        // Mapping at col 0 in section should be offset to col 10 (first line only)
3898        let loc = sm.original_position_for(0, 10).unwrap();
3899        assert_eq!(loc.line, 0);
3900        assert_eq!(loc.column, 0);
3901        // Before the offset should have no mapping
3902        assert!(sm.original_position_for(0, 0).is_none());
3903    }
3904
3905    #[test]
3906    fn indexed_source_map_column_offset_only_first_line() {
3907        // Column offset only applies to the first line of a section
3908        let json = r#"{
3909            "version": 3,
3910            "sections": [
3911                {
3912                    "offset": {"line": 0, "column": 20},
3913                    "map": {
3914                        "version": 3,
3915                        "sources": ["a.js"],
3916                        "names": [],
3917                        "mappings": "AAAA;AAAA"
3918                    }
3919                }
3920            ]
3921        }"#;
3922        let sm = SourceMap::from_json(json).unwrap();
3923        // Line 0: column offset applies
3924        let loc = sm.original_position_for(0, 20).unwrap();
3925        assert_eq!(loc.column, 0);
3926        // Line 1: column offset does NOT apply
3927        let loc = sm.original_position_for(1, 0).unwrap();
3928        assert_eq!(loc.column, 0);
3929    }
3930
3931    #[test]
3932    fn indexed_source_map_empty_section() {
3933        let json = r#"{
3934            "version": 3,
3935            "sections": [
3936                {
3937                    "offset": {"line": 0, "column": 0},
3938                    "map": {
3939                        "version": 3,
3940                        "sources": [],
3941                        "names": [],
3942                        "mappings": ""
3943                    }
3944                },
3945                {
3946                    "offset": {"line": 5, "column": 0},
3947                    "map": {
3948                        "version": 3,
3949                        "sources": ["b.js"],
3950                        "names": [],
3951                        "mappings": "AAAA"
3952                    }
3953                }
3954            ]
3955        }"#;
3956        let sm = SourceMap::from_json(json).unwrap();
3957        assert_eq!(sm.sources.len(), 1);
3958        let loc = sm.original_position_for(5, 0).unwrap();
3959        assert_eq!(sm.source(loc.source), "b.js");
3960    }
3961
3962    #[test]
3963    fn indexed_source_map_with_sources_content() {
3964        let json = r#"{
3965            "version": 3,
3966            "sections": [
3967                {
3968                    "offset": {"line": 0, "column": 0},
3969                    "map": {
3970                        "version": 3,
3971                        "sources": ["a.js"],
3972                        "sourcesContent": ["var a = 1;"],
3973                        "names": [],
3974                        "mappings": "AAAA"
3975                    }
3976                },
3977                {
3978                    "offset": {"line": 5, "column": 0},
3979                    "map": {
3980                        "version": 3,
3981                        "sources": ["b.js"],
3982                        "sourcesContent": ["var b = 2;"],
3983                        "names": [],
3984                        "mappings": "AAAA"
3985                    }
3986                }
3987            ]
3988        }"#;
3989        let sm = SourceMap::from_json(json).unwrap();
3990        assert_eq!(sm.sources_content.len(), 2);
3991        assert_eq!(sm.sources_content[0], Some("var a = 1;".to_string()));
3992        assert_eq!(sm.sources_content[1], Some("var b = 2;".to_string()));
3993    }
3994
3995    #[test]
3996    fn indexed_source_map_with_ignore_list() {
3997        let json = r#"{
3998            "version": 3,
3999            "sections": [
4000                {
4001                    "offset": {"line": 0, "column": 0},
4002                    "map": {
4003                        "version": 3,
4004                        "sources": ["app.js", "vendor.js"],
4005                        "names": [],
4006                        "mappings": "AAAA",
4007                        "ignoreList": [1]
4008                    }
4009                }
4010            ]
4011        }"#;
4012        let sm = SourceMap::from_json(json).unwrap();
4013        assert!(!sm.ignore_list.is_empty());
4014    }
4015
4016    // -- Boundary conditions --
4017
4018    #[test]
4019    fn lookup_max_column_on_line() {
4020        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
4021        let json = build_sourcemap_json(&["a.js"], &[], &mappings_data);
4022        let sm = SourceMap::from_json(&json).unwrap();
4023        // Very large column — should snap to the last mapping on line
4024        let loc = sm.original_position_for(0, u32::MAX - 1).unwrap();
4025        assert_eq!(loc.line, 0);
4026        assert_eq!(loc.column, 0);
4027    }
4028
4029    #[test]
4030    fn mappings_for_line_beyond_end() {
4031        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4032        let sm = SourceMap::from_json(json).unwrap();
4033        assert!(sm.mappings_for_line(u32::MAX).is_empty());
4034    }
4035
4036    #[test]
4037    fn source_with_unicode_path() {
4038        let json =
4039            r#"{"version":3,"sources":["src/日本語.ts"],"names":["変数"],"mappings":"AAAAA"}"#;
4040        let sm = SourceMap::from_json(json).unwrap();
4041        assert_eq!(sm.sources[0], "src/日本語.ts");
4042        assert_eq!(sm.names[0], "変数");
4043        let loc = sm.original_position_for(0, 0).unwrap();
4044        assert_eq!(sm.source(loc.source), "src/日本語.ts");
4045        assert_eq!(sm.name(loc.name.unwrap()), "変数");
4046    }
4047
4048    #[test]
4049    fn to_json_roundtrip_unicode_sources() {
4050        let json = r#"{"version":3,"sources":["src/日本語.ts"],"sourcesContent":["const 変数 = 1;"],"names":["変数"],"mappings":"AAAAA"}"#;
4051        let sm = SourceMap::from_json(json).unwrap();
4052        let output = sm.to_json();
4053        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
4054        let sm2 = SourceMap::from_json(&output).unwrap();
4055        assert_eq!(sm2.sources[0], "src/日本語.ts");
4056        assert_eq!(sm2.sources_content[0], Some("const 変数 = 1;".to_string()));
4057    }
4058
4059    #[test]
4060    fn many_sources_lookup() {
4061        // 100 sources, verify source_index works for all
4062        let sources: Vec<String> = (0..100).map(|i| format!("src/file{i}.js")).collect();
4063        let source_strs: Vec<&str> = sources.iter().map(|s| s.as_str()).collect();
4064        let mappings_data = vec![
4065            sources
4066                .iter()
4067                .enumerate()
4068                .map(|(i, _)| vec![(i * 10) as i64, i as i64, 0, 0])
4069                .collect::<Vec<_>>(),
4070        ];
4071        let json = build_sourcemap_json(&source_strs, &[], &mappings_data);
4072        let sm = SourceMap::from_json(&json).unwrap();
4073
4074        for (i, src) in sources.iter().enumerate() {
4075            assert_eq!(sm.source_index(src), Some(i as u32));
4076        }
4077    }
4078
4079    #[test]
4080    fn clone_sourcemap() {
4081        let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA"}"#;
4082        let sm = SourceMap::from_json(json).unwrap();
4083        let sm2 = sm.clone();
4084        assert_eq!(sm2.sources, sm.sources);
4085        assert_eq!(sm2.mapping_count(), sm.mapping_count());
4086        let loc = sm2.original_position_for(0, 0).unwrap();
4087        assert_eq!(sm2.source(loc.source), "a.js");
4088    }
4089
4090    #[test]
4091    fn parse_debug_id() {
4092        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
4093        let sm = SourceMap::from_json(json).unwrap();
4094        assert_eq!(
4095            sm.debug_id.as_deref(),
4096            Some("85314830-023f-4cf1-a267-535f4e37bb17")
4097        );
4098    }
4099
4100    #[test]
4101    fn parse_debug_id_snake_case() {
4102        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debug_id":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
4103        let sm = SourceMap::from_json(json).unwrap();
4104        assert_eq!(
4105            sm.debug_id.as_deref(),
4106            Some("85314830-023f-4cf1-a267-535f4e37bb17")
4107        );
4108    }
4109
4110    #[test]
4111    fn parse_no_debug_id() {
4112        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4113        let sm = SourceMap::from_json(json).unwrap();
4114        assert_eq!(sm.debug_id, None);
4115    }
4116
4117    #[test]
4118    fn debug_id_roundtrip() {
4119        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
4120        let sm = SourceMap::from_json(json).unwrap();
4121        let output = sm.to_json();
4122        assert!(output.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
4123        let sm2 = SourceMap::from_json(&output).unwrap();
4124        assert_eq!(sm.debug_id, sm2.debug_id);
4125    }
4126
4127    #[test]
4128    fn debug_id_not_in_json_when_absent() {
4129        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4130        let sm = SourceMap::from_json(json).unwrap();
4131        let output = sm.to_json();
4132        assert!(!output.contains("debugId"));
4133    }
4134
4135    /// Generate a test source map JSON with realistic structure.
4136    fn generate_test_sourcemap(lines: usize, segs_per_line: usize, num_sources: usize) -> String {
4137        let sources: Vec<String> = (0..num_sources)
4138            .map(|i| format!("src/file{i}.js"))
4139            .collect();
4140        let names: Vec<String> = (0..20).map(|i| format!("var{i}")).collect();
4141
4142        let mut mappings_parts = Vec::with_capacity(lines);
4143        let mut gen_col;
4144        let mut src: i64 = 0;
4145        let mut src_line: i64 = 0;
4146        let mut src_col: i64;
4147        let mut name: i64 = 0;
4148
4149        for _ in 0..lines {
4150            gen_col = 0i64;
4151            let mut line_parts = Vec::with_capacity(segs_per_line);
4152
4153            for s in 0..segs_per_line {
4154                let gc_delta = 2 + (s as i64 * 3) % 20;
4155                gen_col += gc_delta;
4156
4157                let src_delta = if s % 7 == 0 { 1 } else { 0 };
4158                src = (src + src_delta) % num_sources as i64;
4159
4160                src_line += 1;
4161                src_col = (s as i64 * 5 + 1) % 30;
4162
4163                let has_name = s % 4 == 0;
4164                if has_name {
4165                    name = (name + 1) % names.len() as i64;
4166                }
4167
4168                // Build segment using codec encode
4169                let segment = if has_name {
4170                    srcmap_codec::Segment::five(gen_col, src, src_line, src_col, name)
4171                } else {
4172                    srcmap_codec::Segment::four(gen_col, src, src_line, src_col)
4173                };
4174
4175                line_parts.push(segment);
4176            }
4177
4178            mappings_parts.push(line_parts);
4179        }
4180
4181        let encoded = srcmap_codec::encode(&mappings_parts);
4182
4183        format!(
4184            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
4185            sources
4186                .iter()
4187                .map(|s| format!("\"{s}\""))
4188                .collect::<Vec<_>>()
4189                .join(","),
4190            names
4191                .iter()
4192                .map(|n| format!("\"{n}\""))
4193                .collect::<Vec<_>>()
4194                .join(","),
4195            encoded,
4196        )
4197    }
4198
4199    // ── Bias tests ───────────────────────────────────────────────
4200
4201    /// Map with multiple mappings per line for bias testing:
4202    /// Line 0: col 0 → src:0:0, col 5 → src:0:5, col 10 → src:0:10
4203    fn bias_map() -> &'static str {
4204        // AAAA = 0,0,0,0  KAAK = 5,0,0,5  KAAK = 5,0,0,5 (delta)
4205        r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,KAAK,KAAK"}"#
4206    }
4207
4208    #[test]
4209    fn original_position_glb_exact_match() {
4210        let sm = SourceMap::from_json(bias_map()).unwrap();
4211        let loc = sm
4212            .original_position_for_with_bias(0, 5, Bias::GreatestLowerBound)
4213            .unwrap();
4214        assert_eq!(loc.column, 5);
4215    }
4216
4217    #[test]
4218    fn original_position_glb_snaps_left() {
4219        let sm = SourceMap::from_json(bias_map()).unwrap();
4220        // Column 7 should snap to the mapping at column 5
4221        let loc = sm
4222            .original_position_for_with_bias(0, 7, Bias::GreatestLowerBound)
4223            .unwrap();
4224        assert_eq!(loc.column, 5);
4225    }
4226
4227    #[test]
4228    fn original_position_lub_exact_match() {
4229        let sm = SourceMap::from_json(bias_map()).unwrap();
4230        let loc = sm
4231            .original_position_for_with_bias(0, 5, Bias::LeastUpperBound)
4232            .unwrap();
4233        assert_eq!(loc.column, 5);
4234    }
4235
4236    #[test]
4237    fn original_position_lub_snaps_right() {
4238        let sm = SourceMap::from_json(bias_map()).unwrap();
4239        // Column 3 with LUB should snap to the mapping at column 5
4240        let loc = sm
4241            .original_position_for_with_bias(0, 3, Bias::LeastUpperBound)
4242            .unwrap();
4243        assert_eq!(loc.column, 5);
4244    }
4245
4246    #[test]
4247    fn original_position_lub_before_first() {
4248        let sm = SourceMap::from_json(bias_map()).unwrap();
4249        // Column 0 with LUB should find mapping at column 0
4250        let loc = sm
4251            .original_position_for_with_bias(0, 0, Bias::LeastUpperBound)
4252            .unwrap();
4253        assert_eq!(loc.column, 0);
4254    }
4255
4256    #[test]
4257    fn original_position_lub_after_last() {
4258        let sm = SourceMap::from_json(bias_map()).unwrap();
4259        // Column 15 with LUB should return None (no mapping at or after 15)
4260        let loc = sm.original_position_for_with_bias(0, 15, Bias::LeastUpperBound);
4261        assert!(loc.is_none());
4262    }
4263
4264    #[test]
4265    fn original_position_glb_before_first() {
4266        let sm = SourceMap::from_json(bias_map()).unwrap();
4267        // Column 0 with GLB should find mapping at column 0
4268        let loc = sm
4269            .original_position_for_with_bias(0, 0, Bias::GreatestLowerBound)
4270            .unwrap();
4271        assert_eq!(loc.column, 0);
4272    }
4273
4274    #[test]
4275    fn generated_position_lub() {
4276        let sm = SourceMap::from_json(bias_map()).unwrap();
4277        // LUB: find first generated position at or after original col 3
4278        let loc = sm
4279            .generated_position_for_with_bias("input.js", 0, 3, Bias::LeastUpperBound)
4280            .unwrap();
4281        assert_eq!(loc.column, 5);
4282    }
4283
4284    #[test]
4285    fn generated_position_glb() {
4286        let sm = SourceMap::from_json(bias_map()).unwrap();
4287        // GLB: find last generated position at or before original col 7
4288        let loc = sm
4289            .generated_position_for_with_bias("input.js", 0, 7, Bias::GreatestLowerBound)
4290            .unwrap();
4291        assert_eq!(loc.column, 5);
4292    }
4293
4294    // ── Range mapping tests ──────────────────────────────────────
4295
4296    #[test]
4297    fn map_range_basic() {
4298        let sm = SourceMap::from_json(bias_map()).unwrap();
4299        let range = sm.map_range(0, 0, 0, 10).unwrap();
4300        assert_eq!(range.source, 0);
4301        assert_eq!(range.original_start_line, 0);
4302        assert_eq!(range.original_start_column, 0);
4303        assert_eq!(range.original_end_line, 0);
4304        assert_eq!(range.original_end_column, 10);
4305    }
4306
4307    #[test]
4308    fn map_range_no_mapping() {
4309        let sm = SourceMap::from_json(bias_map()).unwrap();
4310        // Line 5 doesn't exist
4311        let range = sm.map_range(0, 0, 5, 0);
4312        assert!(range.is_none());
4313    }
4314
4315    #[test]
4316    fn map_range_different_sources() {
4317        // Map with two sources: line 0 → src0, line 1 → src1
4318        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA;ACAA"}"#;
4319        let sm = SourceMap::from_json(json).unwrap();
4320        // Start maps to a.js, end maps to b.js → should return None
4321        let range = sm.map_range(0, 0, 1, 0);
4322        assert!(range.is_none());
4323    }
4324
4325    // ── Phase 10 tests ───────────────────────────────────────────
4326
4327    #[test]
4328    fn extension_fields_preserved() {
4329        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_facebook_sources":[[{"names":["<global>"]}]],"x_google_linecount":42}"#;
4330        let sm = SourceMap::from_json(json).unwrap();
4331
4332        assert!(sm.extensions.contains_key("x_facebook_sources"));
4333        assert!(sm.extensions.contains_key("x_google_linecount"));
4334        assert_eq!(
4335            sm.extensions.get("x_google_linecount"),
4336            Some(&serde_json::json!(42))
4337        );
4338
4339        // Round-trip preserves extension fields
4340        let output = sm.to_json();
4341        assert!(output.contains("x_facebook_sources"));
4342        assert!(output.contains("x_google_linecount"));
4343    }
4344
4345    #[test]
4346    fn x_google_ignorelist_fallback() {
4347        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA","x_google_ignoreList":[1]}"#;
4348        let sm = SourceMap::from_json(json).unwrap();
4349        assert_eq!(sm.ignore_list, vec![1]);
4350    }
4351
4352    #[test]
4353    fn ignorelist_takes_precedence_over_x_google() {
4354        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA","ignoreList":[0],"x_google_ignoreList":[1]}"#;
4355        let sm = SourceMap::from_json(json).unwrap();
4356        assert_eq!(sm.ignore_list, vec![0]);
4357    }
4358
4359    #[test]
4360    fn source_mapping_url_external() {
4361        let source = "var a = 1;\n//# sourceMappingURL=app.js.map\n";
4362        let result = parse_source_mapping_url(source).unwrap();
4363        assert_eq!(result, SourceMappingUrl::External("app.js.map".to_string()));
4364    }
4365
4366    #[test]
4367    fn source_mapping_url_inline() {
4368        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
4369        let b64 = base64_encode_simple(json);
4370        let source =
4371            format!("var a = 1;\n//# sourceMappingURL=data:application/json;base64,{b64}\n");
4372        match parse_source_mapping_url(&source).unwrap() {
4373            SourceMappingUrl::Inline(decoded) => {
4374                assert_eq!(decoded, json);
4375            }
4376            _ => panic!("expected inline"),
4377        }
4378    }
4379
4380    #[test]
4381    fn source_mapping_url_at_sign() {
4382        let source = "var a = 1;\n//@ sourceMappingURL=old-style.map";
4383        let result = parse_source_mapping_url(source).unwrap();
4384        assert_eq!(
4385            result,
4386            SourceMappingUrl::External("old-style.map".to_string())
4387        );
4388    }
4389
4390    #[test]
4391    fn source_mapping_url_css_comment() {
4392        let source = "body { }\n/*# sourceMappingURL=styles.css.map */";
4393        let result = parse_source_mapping_url(source).unwrap();
4394        assert_eq!(
4395            result,
4396            SourceMappingUrl::External("styles.css.map".to_string())
4397        );
4398    }
4399
4400    #[test]
4401    fn source_mapping_url_none() {
4402        let source = "var a = 1;";
4403        assert!(parse_source_mapping_url(source).is_none());
4404    }
4405
4406    #[test]
4407    fn exclude_content_option() {
4408        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":[],"mappings":"AAAA"}"#;
4409        let sm = SourceMap::from_json(json).unwrap();
4410
4411        let with_content = sm.to_json();
4412        assert!(with_content.contains("sourcesContent"));
4413
4414        let without_content = sm.to_json_with_options(true);
4415        assert!(!without_content.contains("sourcesContent"));
4416    }
4417
4418    #[test]
4419    fn validate_deep_clean_map() {
4420        let sm = SourceMap::from_json(simple_map()).unwrap();
4421        let warnings = validate_deep(&sm);
4422        assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
4423    }
4424
4425    #[test]
4426    fn validate_deep_unreferenced_source() {
4427        // Source "unused.js" has no mappings pointing to it
4428        let json =
4429            r#"{"version":3,"sources":["used.js","unused.js"],"names":[],"mappings":"AAAA"}"#;
4430        let sm = SourceMap::from_json(json).unwrap();
4431        let warnings = validate_deep(&sm);
4432        assert!(warnings.iter().any(|w| w.contains("unused.js")));
4433    }
4434
4435    // ── from_parts tests ──────────────────────────────────────────
4436
4437    #[test]
4438    fn from_parts_basic() {
4439        let mappings = vec![
4440            Mapping {
4441                generated_line: 0,
4442                generated_column: 0,
4443                source: 0,
4444                original_line: 0,
4445                original_column: 0,
4446                name: NO_NAME,
4447                is_range_mapping: false,
4448            },
4449            Mapping {
4450                generated_line: 1,
4451                generated_column: 4,
4452                source: 0,
4453                original_line: 1,
4454                original_column: 2,
4455                name: NO_NAME,
4456                is_range_mapping: false,
4457            },
4458        ];
4459
4460        let sm = SourceMap::from_parts(
4461            Some("out.js".to_string()),
4462            None,
4463            vec!["input.js".to_string()],
4464            vec![Some("var x = 1;".to_string())],
4465            vec![],
4466            mappings,
4467            vec![],
4468            None,
4469            None,
4470        );
4471
4472        assert_eq!(sm.line_count(), 2);
4473        assert_eq!(sm.mapping_count(), 2);
4474
4475        let loc = sm.original_position_for(0, 0).unwrap();
4476        assert_eq!(loc.source, 0);
4477        assert_eq!(loc.line, 0);
4478        assert_eq!(loc.column, 0);
4479
4480        let loc = sm.original_position_for(1, 4).unwrap();
4481        assert_eq!(loc.line, 1);
4482        assert_eq!(loc.column, 2);
4483    }
4484
4485    #[test]
4486    fn from_parts_empty() {
4487        let sm = SourceMap::from_parts(
4488            None,
4489            None,
4490            vec![],
4491            vec![],
4492            vec![],
4493            vec![],
4494            vec![],
4495            None,
4496            None,
4497        );
4498        assert_eq!(sm.line_count(), 0);
4499        assert_eq!(sm.mapping_count(), 0);
4500        assert!(sm.original_position_for(0, 0).is_none());
4501    }
4502
4503    #[test]
4504    fn from_parts_with_names() {
4505        let mappings = vec![Mapping {
4506            generated_line: 0,
4507            generated_column: 0,
4508            source: 0,
4509            original_line: 0,
4510            original_column: 0,
4511            name: 0,
4512            is_range_mapping: false,
4513        }];
4514
4515        let sm = SourceMap::from_parts(
4516            None,
4517            None,
4518            vec!["input.js".to_string()],
4519            vec![],
4520            vec!["myVar".to_string()],
4521            mappings,
4522            vec![],
4523            None,
4524            None,
4525        );
4526
4527        let loc = sm.original_position_for(0, 0).unwrap();
4528        assert_eq!(loc.name, Some(0));
4529        assert_eq!(sm.name(0), "myVar");
4530    }
4531
4532    #[test]
4533    fn from_parts_roundtrip_via_json() {
4534        let json = generate_test_sourcemap(50, 10, 3);
4535        let sm = SourceMap::from_json(&json).unwrap();
4536
4537        let sm2 = SourceMap::from_parts(
4538            sm.file.clone(),
4539            sm.source_root.clone(),
4540            sm.sources.clone(),
4541            sm.sources_content.clone(),
4542            sm.names.clone(),
4543            sm.all_mappings().to_vec(),
4544            sm.ignore_list.clone(),
4545            sm.debug_id.clone(),
4546            None,
4547        );
4548
4549        assert_eq!(sm2.mapping_count(), sm.mapping_count());
4550        assert_eq!(sm2.line_count(), sm.line_count());
4551
4552        // Spot-check lookups
4553        for m in sm.all_mappings() {
4554            if m.source != NO_SOURCE {
4555                let a = sm.original_position_for(m.generated_line, m.generated_column);
4556                let b = sm2.original_position_for(m.generated_line, m.generated_column);
4557                match (a, b) {
4558                    (Some(a), Some(b)) => {
4559                        assert_eq!(a.source, b.source);
4560                        assert_eq!(a.line, b.line);
4561                        assert_eq!(a.column, b.column);
4562                    }
4563                    (None, None) => {}
4564                    _ => panic!("mismatch at ({}, {})", m.generated_line, m.generated_column),
4565                }
4566            }
4567        }
4568    }
4569
4570    #[test]
4571    fn from_parts_reverse_lookup() {
4572        let mappings = vec![
4573            Mapping {
4574                generated_line: 0,
4575                generated_column: 0,
4576                source: 0,
4577                original_line: 10,
4578                original_column: 5,
4579                name: NO_NAME,
4580                is_range_mapping: false,
4581            },
4582            Mapping {
4583                generated_line: 1,
4584                generated_column: 8,
4585                source: 0,
4586                original_line: 20,
4587                original_column: 0,
4588                name: NO_NAME,
4589                is_range_mapping: false,
4590            },
4591        ];
4592
4593        let sm = SourceMap::from_parts(
4594            None,
4595            None,
4596            vec!["src.js".to_string()],
4597            vec![],
4598            vec![],
4599            mappings,
4600            vec![],
4601            None,
4602            None,
4603        );
4604
4605        let loc = sm.generated_position_for("src.js", 10, 5).unwrap();
4606        assert_eq!(loc.line, 0);
4607        assert_eq!(loc.column, 0);
4608
4609        let loc = sm.generated_position_for("src.js", 20, 0).unwrap();
4610        assert_eq!(loc.line, 1);
4611        assert_eq!(loc.column, 8);
4612    }
4613
4614    #[test]
4615    fn from_parts_sparse_lines() {
4616        let mappings = vec![
4617            Mapping {
4618                generated_line: 0,
4619                generated_column: 0,
4620                source: 0,
4621                original_line: 0,
4622                original_column: 0,
4623                name: NO_NAME,
4624                is_range_mapping: false,
4625            },
4626            Mapping {
4627                generated_line: 5,
4628                generated_column: 0,
4629                source: 0,
4630                original_line: 5,
4631                original_column: 0,
4632                name: NO_NAME,
4633                is_range_mapping: false,
4634            },
4635        ];
4636
4637        let sm = SourceMap::from_parts(
4638            None,
4639            None,
4640            vec!["src.js".to_string()],
4641            vec![],
4642            vec![],
4643            mappings,
4644            vec![],
4645            None,
4646            None,
4647        );
4648
4649        assert_eq!(sm.line_count(), 6);
4650        assert!(sm.original_position_for(0, 0).is_some());
4651        assert!(sm.original_position_for(2, 0).is_none());
4652        assert!(sm.original_position_for(5, 0).is_some());
4653    }
4654
4655    // ── from_json_lines tests ────────────────────────────────────
4656
4657    #[test]
4658    fn from_json_lines_basic() {
4659        let json = generate_test_sourcemap(10, 5, 2);
4660        let sm_full = SourceMap::from_json(&json).unwrap();
4661
4662        // Decode only lines 3..7
4663        let sm_partial = SourceMap::from_json_lines(&json, 3, 7).unwrap();
4664
4665        // Verify mappings for lines in range match
4666        for line in 3..7u32 {
4667            let full_mappings = sm_full.mappings_for_line(line);
4668            let partial_mappings = sm_partial.mappings_for_line(line);
4669            assert_eq!(
4670                full_mappings.len(),
4671                partial_mappings.len(),
4672                "line {line} mapping count mismatch"
4673            );
4674            for (a, b) in full_mappings.iter().zip(partial_mappings.iter()) {
4675                assert_eq!(a.generated_column, b.generated_column);
4676                assert_eq!(a.source, b.source);
4677                assert_eq!(a.original_line, b.original_line);
4678                assert_eq!(a.original_column, b.original_column);
4679                assert_eq!(a.name, b.name);
4680            }
4681        }
4682    }
4683
4684    #[test]
4685    fn from_json_lines_first_lines() {
4686        let json = generate_test_sourcemap(10, 5, 2);
4687        let sm_full = SourceMap::from_json(&json).unwrap();
4688        let sm_partial = SourceMap::from_json_lines(&json, 0, 3).unwrap();
4689
4690        for line in 0..3u32 {
4691            let full_mappings = sm_full.mappings_for_line(line);
4692            let partial_mappings = sm_partial.mappings_for_line(line);
4693            assert_eq!(full_mappings.len(), partial_mappings.len());
4694        }
4695    }
4696
4697    #[test]
4698    fn from_json_lines_last_lines() {
4699        let json = generate_test_sourcemap(10, 5, 2);
4700        let sm_full = SourceMap::from_json(&json).unwrap();
4701        let sm_partial = SourceMap::from_json_lines(&json, 7, 10).unwrap();
4702
4703        for line in 7..10u32 {
4704            let full_mappings = sm_full.mappings_for_line(line);
4705            let partial_mappings = sm_partial.mappings_for_line(line);
4706            assert_eq!(full_mappings.len(), partial_mappings.len(), "line {line}");
4707        }
4708    }
4709
4710    #[test]
4711    fn from_json_lines_empty_range() {
4712        let json = generate_test_sourcemap(10, 5, 2);
4713        let sm = SourceMap::from_json_lines(&json, 5, 5).unwrap();
4714        assert_eq!(sm.mapping_count(), 0);
4715    }
4716
4717    #[test]
4718    fn from_json_lines_beyond_end() {
4719        let json = generate_test_sourcemap(5, 3, 1);
4720        // Request lines beyond what exists
4721        let sm = SourceMap::from_json_lines(&json, 3, 100).unwrap();
4722        // Should have mappings for lines 3 and 4 (the ones that exist in the range)
4723        assert!(sm.mapping_count() > 0);
4724    }
4725
4726    #[test]
4727    fn from_json_lines_single_line() {
4728        let json = generate_test_sourcemap(10, 5, 2);
4729        let sm_full = SourceMap::from_json(&json).unwrap();
4730        let sm_partial = SourceMap::from_json_lines(&json, 5, 6).unwrap();
4731
4732        let full_mappings = sm_full.mappings_for_line(5);
4733        let partial_mappings = sm_partial.mappings_for_line(5);
4734        assert_eq!(full_mappings.len(), partial_mappings.len());
4735    }
4736
4737    // ── LazySourceMap tests ──────────────────────────────────────
4738
4739    #[test]
4740    fn lazy_basic_lookup() {
4741        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
4742        let sm = LazySourceMap::from_json(json).unwrap();
4743
4744        assert_eq!(sm.line_count(), 2);
4745        assert_eq!(sm.sources, vec!["input.js"]);
4746
4747        let loc = sm.original_position_for(0, 0).unwrap();
4748        assert_eq!(sm.source(loc.source), "input.js");
4749        assert_eq!(loc.line, 0);
4750        assert_eq!(loc.column, 0);
4751    }
4752
4753    #[test]
4754    fn lazy_multiple_lines() {
4755        let json = generate_test_sourcemap(20, 5, 3);
4756        let sm_eager = SourceMap::from_json(&json).unwrap();
4757        let sm_lazy = LazySourceMap::from_json(&json).unwrap();
4758
4759        assert_eq!(sm_lazy.line_count(), sm_eager.line_count());
4760
4761        // Verify lookups match for every mapping
4762        for m in sm_eager.all_mappings() {
4763            if m.source == NO_SOURCE {
4764                continue;
4765            }
4766            let eager_loc = sm_eager
4767                .original_position_for(m.generated_line, m.generated_column)
4768                .unwrap();
4769            let lazy_loc = sm_lazy
4770                .original_position_for(m.generated_line, m.generated_column)
4771                .unwrap();
4772            assert_eq!(eager_loc.source, lazy_loc.source);
4773            assert_eq!(eager_loc.line, lazy_loc.line);
4774            assert_eq!(eager_loc.column, lazy_loc.column);
4775            assert_eq!(eager_loc.name, lazy_loc.name);
4776        }
4777    }
4778
4779    #[test]
4780    fn lazy_empty_mappings() {
4781        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
4782        let sm = LazySourceMap::from_json(json).unwrap();
4783        assert_eq!(sm.line_count(), 0);
4784        assert!(sm.original_position_for(0, 0).is_none());
4785    }
4786
4787    #[test]
4788    fn lazy_empty_lines() {
4789        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
4790        let sm = LazySourceMap::from_json(json).unwrap();
4791        assert_eq!(sm.line_count(), 4);
4792
4793        assert!(sm.original_position_for(0, 0).is_some());
4794        assert!(sm.original_position_for(1, 0).is_none());
4795        assert!(sm.original_position_for(2, 0).is_none());
4796        assert!(sm.original_position_for(3, 0).is_some());
4797    }
4798
4799    #[test]
4800    fn lazy_decode_line_caching() {
4801        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,KACA;AACA"}"#;
4802        let sm = LazySourceMap::from_json(json).unwrap();
4803
4804        // First call decodes
4805        let line0_a = sm.decode_line(0).unwrap();
4806        // Second call should return cached
4807        let line0_b = sm.decode_line(0).unwrap();
4808        assert_eq!(line0_a.len(), line0_b.len());
4809        assert_eq!(line0_a[0].generated_column, line0_b[0].generated_column);
4810    }
4811
4812    #[test]
4813    fn lazy_with_names() {
4814        let json = r#"{"version":3,"sources":["input.js"],"names":["foo","bar"],"mappings":"AAAAA,KACAC"}"#;
4815        let sm = LazySourceMap::from_json(json).unwrap();
4816
4817        let loc = sm.original_position_for(0, 0).unwrap();
4818        assert_eq!(loc.name, Some(0));
4819        assert_eq!(sm.name(0), "foo");
4820
4821        let loc = sm.original_position_for(0, 5).unwrap();
4822        assert_eq!(loc.name, Some(1));
4823        assert_eq!(sm.name(1), "bar");
4824    }
4825
4826    #[test]
4827    fn lazy_nonexistent_line() {
4828        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4829        let sm = LazySourceMap::from_json(json).unwrap();
4830        assert!(sm.original_position_for(99, 0).is_none());
4831        let line = sm.decode_line(99).unwrap();
4832        assert!(line.is_empty());
4833    }
4834
4835    #[test]
4836    fn lazy_into_sourcemap() {
4837        let json = generate_test_sourcemap(20, 5, 3);
4838        let sm_eager = SourceMap::from_json(&json).unwrap();
4839        let sm_lazy = LazySourceMap::from_json(&json).unwrap();
4840        let sm_converted = sm_lazy.into_sourcemap().unwrap();
4841
4842        assert_eq!(sm_converted.mapping_count(), sm_eager.mapping_count());
4843        assert_eq!(sm_converted.line_count(), sm_eager.line_count());
4844
4845        // Verify all lookups match
4846        for m in sm_eager.all_mappings() {
4847            let a = sm_eager.original_position_for(m.generated_line, m.generated_column);
4848            let b = sm_converted.original_position_for(m.generated_line, m.generated_column);
4849            match (a, b) {
4850                (Some(a), Some(b)) => {
4851                    assert_eq!(a.source, b.source);
4852                    assert_eq!(a.line, b.line);
4853                    assert_eq!(a.column, b.column);
4854                }
4855                (None, None) => {}
4856                _ => panic!("mismatch at ({}, {})", m.generated_line, m.generated_column),
4857            }
4858        }
4859    }
4860
4861    #[test]
4862    fn lazy_source_index_lookup() {
4863        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA;ACAA"}"#;
4864        let sm = LazySourceMap::from_json(json).unwrap();
4865        assert_eq!(sm.source_index("a.js"), Some(0));
4866        assert_eq!(sm.source_index("b.js"), Some(1));
4867        assert_eq!(sm.source_index("c.js"), None);
4868    }
4869
4870    #[test]
4871    fn lazy_mappings_for_line() {
4872        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,KACA;AACA"}"#;
4873        let sm = LazySourceMap::from_json(json).unwrap();
4874
4875        let line0 = sm.mappings_for_line(0);
4876        assert_eq!(line0.len(), 2);
4877
4878        let line1 = sm.mappings_for_line(1);
4879        assert_eq!(line1.len(), 1);
4880
4881        let line99 = sm.mappings_for_line(99);
4882        assert!(line99.is_empty());
4883    }
4884
4885    #[test]
4886    fn lazy_large_map_selective_decode() {
4887        // Generate a large map but only decode a few lines
4888        let json = generate_test_sourcemap(100, 10, 5);
4889        let sm_eager = SourceMap::from_json(&json).unwrap();
4890        let sm_lazy = LazySourceMap::from_json(&json).unwrap();
4891
4892        // Only decode lines 50 and 75
4893        for line in [50, 75] {
4894            let eager_mappings = sm_eager.mappings_for_line(line);
4895            let lazy_mappings = sm_lazy.mappings_for_line(line);
4896            assert_eq!(
4897                eager_mappings.len(),
4898                lazy_mappings.len(),
4899                "line {line} count mismatch"
4900            );
4901            for (a, b) in eager_mappings.iter().zip(lazy_mappings.iter()) {
4902                assert_eq!(a.generated_column, b.generated_column);
4903                assert_eq!(a.source, b.source);
4904                assert_eq!(a.original_line, b.original_line);
4905                assert_eq!(a.original_column, b.original_column);
4906                assert_eq!(a.name, b.name);
4907            }
4908        }
4909    }
4910
4911    #[test]
4912    fn lazy_single_field_segments() {
4913        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
4914        let sm = LazySourceMap::from_json(json).unwrap();
4915
4916        // First segment is single-field (no source info)
4917        assert!(sm.original_position_for(0, 0).is_none());
4918        // Second segment has source info
4919        let loc = sm.original_position_for(0, 5).unwrap();
4920        assert_eq!(loc.source, 0);
4921    }
4922
4923    // ── Coverage gap tests ──────────────────────────────────────────
4924
4925    #[test]
4926    fn parse_error_display_vlq() {
4927        let err = ParseError::Vlq(srcmap_codec::DecodeError::UnexpectedEof { offset: 3 });
4928        assert!(err.to_string().contains("VLQ decode error"));
4929    }
4930
4931    #[test]
4932    fn parse_error_display_scopes() {
4933        let err = ParseError::Scopes(srcmap_scopes::ScopesError::UnclosedScope);
4934        assert!(err.to_string().contains("scopes decode error"));
4935    }
4936
4937    #[test]
4938    fn indexed_map_with_names_in_sections() {
4939        let json = r#"{
4940            "version": 3,
4941            "sections": [
4942                {
4943                    "offset": {"line": 0, "column": 0},
4944                    "map": {
4945                        "version": 3,
4946                        "sources": ["a.js"],
4947                        "names": ["foo"],
4948                        "mappings": "AAAAA"
4949                    }
4950                },
4951                {
4952                    "offset": {"line": 1, "column": 0},
4953                    "map": {
4954                        "version": 3,
4955                        "sources": ["a.js"],
4956                        "names": ["foo"],
4957                        "mappings": "AAAAA"
4958                    }
4959                }
4960            ]
4961        }"#;
4962        let sm = SourceMap::from_json(json).unwrap();
4963        // Sources and names should be deduplicated
4964        assert_eq!(sm.sources.len(), 1);
4965        assert_eq!(sm.names.len(), 1);
4966    }
4967
4968    #[test]
4969    fn indexed_map_with_ignore_list() {
4970        let json = r#"{
4971            "version": 3,
4972            "sections": [
4973                {
4974                    "offset": {"line": 0, "column": 0},
4975                    "map": {
4976                        "version": 3,
4977                        "sources": ["vendor.js"],
4978                        "names": [],
4979                        "mappings": "AAAA",
4980                        "ignoreList": [0]
4981                    }
4982                }
4983            ]
4984        }"#;
4985        let sm = SourceMap::from_json(json).unwrap();
4986        assert_eq!(sm.ignore_list, vec![0]);
4987    }
4988
4989    #[test]
4990    fn indexed_map_with_generated_only_segment() {
4991        // Section with a generated-only (1-field) segment
4992        let json = r#"{
4993            "version": 3,
4994            "sections": [
4995                {
4996                    "offset": {"line": 0, "column": 0},
4997                    "map": {
4998                        "version": 3,
4999                        "sources": ["a.js"],
5000                        "names": [],
5001                        "mappings": "A,AAAA"
5002                    }
5003                }
5004            ]
5005        }"#;
5006        let sm = SourceMap::from_json(json).unwrap();
5007        assert!(sm.mapping_count() >= 1);
5008    }
5009
5010    #[test]
5011    fn indexed_map_empty_mappings() {
5012        let json = r#"{
5013            "version": 3,
5014            "sections": [
5015                {
5016                    "offset": {"line": 0, "column": 0},
5017                    "map": {
5018                        "version": 3,
5019                        "sources": [],
5020                        "names": [],
5021                        "mappings": ""
5022                    }
5023                }
5024            ]
5025        }"#;
5026        let sm = SourceMap::from_json(json).unwrap();
5027        assert_eq!(sm.mapping_count(), 0);
5028    }
5029
5030    #[test]
5031    fn generated_position_glb_exact_match() {
5032        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,EAAE,OAAO"}"#;
5033        let sm = SourceMap::from_json(json).unwrap();
5034
5035        let loc = sm.generated_position_for_with_bias("a.js", 0, 0, Bias::GreatestLowerBound);
5036        assert!(loc.is_some());
5037        assert_eq!(loc.unwrap().column, 0);
5038    }
5039
5040    #[test]
5041    fn generated_position_glb_no_exact_match() {
5042        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,EAAE"}"#;
5043        let sm = SourceMap::from_json(json).unwrap();
5044
5045        // Look for position between two mappings
5046        let loc = sm.generated_position_for_with_bias("a.js", 0, 0, Bias::GreatestLowerBound);
5047        assert!(loc.is_some());
5048    }
5049
5050    #[test]
5051    fn generated_position_glb_wrong_source() {
5052        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5053        let sm = SourceMap::from_json(json).unwrap();
5054
5055        // GLB for position in b.js that doesn't exist at that location
5056        let loc = sm.generated_position_for_with_bias("b.js", 5, 0, Bias::GreatestLowerBound);
5057        // Should find something or nothing depending on whether there's a mapping before
5058        // The key is that source filtering works
5059        if let Some(l) = loc {
5060            // Verify returned position is valid (line 0 is the only generated line)
5061            assert_eq!(l.line, 0);
5062        }
5063    }
5064
5065    #[test]
5066    fn generated_position_lub_wrong_source() {
5067        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5068        let sm = SourceMap::from_json(json).unwrap();
5069
5070        // LUB for non-existent source
5071        let loc =
5072            sm.generated_position_for_with_bias("nonexistent.js", 0, 0, Bias::LeastUpperBound);
5073        assert!(loc.is_none());
5074    }
5075
5076    #[test]
5077    fn to_json_with_ignore_list() {
5078        let json = r#"{"version":3,"sources":["vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[0]}"#;
5079        let sm = SourceMap::from_json(json).unwrap();
5080        let output = sm.to_json();
5081        assert!(output.contains("\"ignoreList\":[0]"));
5082    }
5083
5084    #[test]
5085    fn to_json_with_extensions() {
5086        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom":"test_value"}"#;
5087        let sm = SourceMap::from_json(json).unwrap();
5088        let output = sm.to_json();
5089        assert!(output.contains("x_custom"));
5090        assert!(output.contains("test_value"));
5091    }
5092
5093    #[test]
5094    fn from_parts_empty_mappings() {
5095        let sm = SourceMap::from_parts(
5096            None,
5097            None,
5098            vec!["a.js".to_string()],
5099            vec![Some("content".to_string())],
5100            vec![],
5101            vec![],
5102            vec![],
5103            None,
5104            None,
5105        );
5106        assert_eq!(sm.mapping_count(), 0);
5107        assert_eq!(sm.sources, vec!["a.js"]);
5108    }
5109
5110    #[test]
5111    fn from_vlq_basic() {
5112        let sm = SourceMap::from_vlq(
5113            "AAAA;AACA",
5114            vec!["a.js".to_string()],
5115            vec![],
5116            Some("out.js".to_string()),
5117            None,
5118            vec![Some("content".to_string())],
5119            vec![],
5120            None,
5121        )
5122        .unwrap();
5123
5124        assert_eq!(sm.file.as_deref(), Some("out.js"));
5125        assert_eq!(sm.sources, vec!["a.js"]);
5126        let loc = sm.original_position_for(0, 0).unwrap();
5127        assert_eq!(sm.source(loc.source), "a.js");
5128        assert_eq!(loc.line, 0);
5129    }
5130
5131    #[test]
5132    fn from_json_lines_basic_coverage() {
5133        let json =
5134            r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA"}"#;
5135        let sm = SourceMap::from_json_lines(json, 1, 3).unwrap();
5136        // Should have mappings for lines 1 and 2
5137        assert!(sm.original_position_for(1, 0).is_some());
5138        assert!(sm.original_position_for(2, 0).is_some());
5139    }
5140
5141    #[test]
5142    fn from_json_lines_with_source_root() {
5143        let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA;AACA"}"#;
5144        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5145        assert_eq!(sm.sources[0], "src/a.js");
5146    }
5147
5148    #[test]
5149    fn from_json_lines_with_null_source() {
5150        let json = r#"{"version":3,"sources":[null,"a.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5151        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5152        assert_eq!(sm.sources.len(), 2);
5153    }
5154
5155    #[test]
5156    fn json_escaping_special_chars_sourcemap() {
5157        // Build a source map with special chars in source name and content via JSON
5158        // The source name has a newline, the content has \r\n, tab, quotes, backslash, and control char
5159        let json = r#"{"version":3,"sources":["path/with\nnewline.js"],"sourcesContent":["line1\r\nline2\t\"quoted\"\\\u0001"],"names":[],"mappings":"AAAA"}"#;
5160        let sm = SourceMap::from_json(json).unwrap();
5161        // Roundtrip through to_json and re-parse
5162        let output = sm.to_json();
5163        let sm2 = SourceMap::from_json(&output).unwrap();
5164        assert_eq!(sm.sources[0], sm2.sources[0]);
5165        assert_eq!(sm.sources_content[0], sm2.sources_content[0]);
5166    }
5167
5168    #[test]
5169    fn to_json_exclude_content() {
5170        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":[],"mappings":"AAAA"}"#;
5171        let sm = SourceMap::from_json(json).unwrap();
5172        let output = sm.to_json_with_options(true);
5173        assert!(!output.contains("sourcesContent"));
5174        let output_with = sm.to_json_with_options(false);
5175        assert!(output_with.contains("sourcesContent"));
5176    }
5177
5178    #[test]
5179    fn encode_mappings_with_name() {
5180        // Ensure encode_mappings handles the name field (5th VLQ)
5181        let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAAA"}"#;
5182        let sm = SourceMap::from_json(json).unwrap();
5183        let encoded = sm.encode_mappings();
5184        assert_eq!(encoded, "AAAAA");
5185    }
5186
5187    #[test]
5188    fn encode_mappings_generated_only() {
5189        // Generated-only segments (NO_SOURCE) in encode
5190        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,AAAA"}"#;
5191        let sm = SourceMap::from_json(json).unwrap();
5192        let encoded = sm.encode_mappings();
5193        let roundtrip = SourceMap::from_json(&format!(
5194            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"{}"}}"#,
5195            encoded
5196        ))
5197        .unwrap();
5198        assert_eq!(roundtrip.mapping_count(), sm.mapping_count());
5199    }
5200
5201    #[test]
5202    fn map_range_single_result() {
5203        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,EAAC,OAAO"}"#;
5204        let sm = SourceMap::from_json(json).unwrap();
5205        // map_range from col 0 to a mapped column
5206        let result = sm.map_range(0, 0, 0, 1);
5207        assert!(result.is_some());
5208        let range = result.unwrap();
5209        assert_eq!(range.source, 0);
5210    }
5211
5212    #[test]
5213    fn scopes_in_from_json() {
5214        // Source map with scopes field - build scopes string, then embed in JSON
5215        let info = srcmap_scopes::ScopeInfo {
5216            scopes: vec![Some(srcmap_scopes::OriginalScope {
5217                start: srcmap_scopes::Position { line: 0, column: 0 },
5218                end: srcmap_scopes::Position { line: 5, column: 0 },
5219                name: None,
5220                kind: None,
5221                is_stack_frame: false,
5222                variables: vec![],
5223                children: vec![],
5224            })],
5225            ranges: vec![],
5226        };
5227        let mut names = vec![];
5228        let scopes_str = srcmap_scopes::encode_scopes(&info, &mut names);
5229
5230        let json = format!(
5231            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"{scopes_str}"}}"#
5232        );
5233
5234        let sm = SourceMap::from_json(&json).unwrap();
5235        assert!(sm.scopes.is_some());
5236    }
5237
5238    #[test]
5239    fn from_json_lines_with_scopes() {
5240        let info = srcmap_scopes::ScopeInfo {
5241            scopes: vec![Some(srcmap_scopes::OriginalScope {
5242                start: srcmap_scopes::Position { line: 0, column: 0 },
5243                end: srcmap_scopes::Position { line: 5, column: 0 },
5244                name: None,
5245                kind: None,
5246                is_stack_frame: false,
5247                variables: vec![],
5248                children: vec![],
5249            })],
5250            ranges: vec![],
5251        };
5252        let mut names = vec![];
5253        let scopes_str = srcmap_scopes::encode_scopes(&info, &mut names);
5254        let json = format!(
5255            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA","scopes":"{scopes_str}"}}"#
5256        );
5257        let sm = SourceMap::from_json_lines(&json, 0, 2).unwrap();
5258        assert!(sm.scopes.is_some());
5259    }
5260
5261    #[test]
5262    fn from_json_lines_with_extensions() {
5263        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom":"val","not_x":"skip"}"#;
5264        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5265        assert!(sm.extensions.contains_key("x_custom"));
5266        assert!(!sm.extensions.contains_key("not_x"));
5267    }
5268
5269    #[test]
5270    fn lazy_sourcemap_version_error() {
5271        let json = r#"{"version":2,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5272        let err = LazySourceMap::from_json(json).unwrap_err();
5273        assert!(matches!(err, ParseError::InvalidVersion(2)));
5274    }
5275
5276    #[test]
5277    fn lazy_sourcemap_with_source_root() {
5278        let json =
5279            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5280        let sm = LazySourceMap::from_json(json).unwrap();
5281        assert_eq!(sm.sources[0], "src/a.js");
5282    }
5283
5284    #[test]
5285    fn lazy_sourcemap_with_ignore_list_and_extensions() {
5286        let json = r#"{"version":3,"sources":["v.js"],"names":[],"mappings":"AAAA","ignoreList":[0],"x_custom":"val","not_x":"skip"}"#;
5287        let sm = LazySourceMap::from_json(json).unwrap();
5288        assert_eq!(sm.ignore_list, vec![0]);
5289        assert!(sm.extensions.contains_key("x_custom"));
5290        assert!(!sm.extensions.contains_key("not_x"));
5291    }
5292
5293    #[test]
5294    fn lazy_sourcemap_with_scopes() {
5295        let info = srcmap_scopes::ScopeInfo {
5296            scopes: vec![Some(srcmap_scopes::OriginalScope {
5297                start: srcmap_scopes::Position { line: 0, column: 0 },
5298                end: srcmap_scopes::Position { line: 5, column: 0 },
5299                name: None,
5300                kind: None,
5301                is_stack_frame: false,
5302                variables: vec![],
5303                children: vec![],
5304            })],
5305            ranges: vec![],
5306        };
5307        let mut names = vec![];
5308        let scopes_str = srcmap_scopes::encode_scopes(&info, &mut names);
5309        let json = format!(
5310            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"{scopes_str}"}}"#
5311        );
5312        let sm = LazySourceMap::from_json(&json).unwrap();
5313        assert!(sm.scopes.is_some());
5314    }
5315
5316    #[test]
5317    fn lazy_sourcemap_null_source() {
5318        let json = r#"{"version":3,"sources":[null,"a.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5319        let sm = LazySourceMap::from_json(json).unwrap();
5320        assert_eq!(sm.sources.len(), 2);
5321    }
5322
5323    #[test]
5324    fn indexed_map_multi_line_section() {
5325        // Multi-line section to exercise line_offsets building in from_sections
5326        let json = r#"{
5327            "version": 3,
5328            "sections": [
5329                {
5330                    "offset": {"line": 0, "column": 0},
5331                    "map": {
5332                        "version": 3,
5333                        "sources": ["a.js"],
5334                        "names": [],
5335                        "mappings": "AAAA;AACA;AACA"
5336                    }
5337                },
5338                {
5339                    "offset": {"line": 5, "column": 0},
5340                    "map": {
5341                        "version": 3,
5342                        "sources": ["b.js"],
5343                        "names": [],
5344                        "mappings": "AAAA;AACA"
5345                    }
5346                }
5347            ]
5348        }"#;
5349        let sm = SourceMap::from_json(json).unwrap();
5350        assert!(sm.original_position_for(0, 0).is_some());
5351        assert!(sm.original_position_for(5, 0).is_some());
5352    }
5353
5354    #[test]
5355    fn source_mapping_url_extraction() {
5356        // External URL
5357        let input = "var x = 1;\n//# sourceMappingURL=bundle.js.map";
5358        let url = parse_source_mapping_url(input);
5359        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "bundle.js.map"));
5360
5361        // CSS comment style
5362        let input = "body { }\n/*# sourceMappingURL=style.css.map */";
5363        let url = parse_source_mapping_url(input);
5364        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "style.css.map"));
5365
5366        // @ sign variant
5367        let input = "var x;\n//@ sourceMappingURL=old-style.map";
5368        let url = parse_source_mapping_url(input);
5369        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "old-style.map"));
5370
5371        // CSS @ variant
5372        let input = "body{}\n/*@ sourceMappingURL=old-css.map */";
5373        let url = parse_source_mapping_url(input);
5374        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "old-css.map"));
5375
5376        // No URL
5377        let input = "var x = 1;";
5378        let url = parse_source_mapping_url(input);
5379        assert!(url.is_none());
5380
5381        // Empty URL
5382        let input = "//# sourceMappingURL=";
5383        let url = parse_source_mapping_url(input);
5384        assert!(url.is_none());
5385
5386        // Inline data URI
5387        let map_json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5388        let encoded = base64_encode_simple(map_json);
5389        let input = format!("var x;\n//# sourceMappingURL=data:application/json;base64,{encoded}");
5390        let url = parse_source_mapping_url(&input);
5391        assert!(matches!(url, Some(SourceMappingUrl::Inline(_))));
5392    }
5393
5394    #[test]
5395    fn validate_deep_unreferenced_coverage() {
5396        // Map with an unreferenced source
5397        let sm = SourceMap::from_parts(
5398            None,
5399            None,
5400            vec!["used.js".to_string(), "unused.js".to_string()],
5401            vec![None, None],
5402            vec![],
5403            vec![Mapping {
5404                generated_line: 0,
5405                generated_column: 0,
5406                source: 0,
5407                original_line: 0,
5408                original_column: 0,
5409                name: NO_NAME,
5410                is_range_mapping: false,
5411            }],
5412            vec![],
5413            None,
5414            None,
5415        );
5416        let warnings = validate_deep(&sm);
5417        assert!(warnings.iter().any(|w| w.contains("unreferenced")));
5418    }
5419
5420    #[test]
5421    fn from_json_lines_generated_only_segment() {
5422        // from_json_lines with 1-field segments to exercise the generated-only branch
5423        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,AAAA;AACA"}"#;
5424        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5425        assert!(sm.mapping_count() >= 2);
5426    }
5427
5428    #[test]
5429    fn from_json_lines_with_names() {
5430        let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAAA;AACAA"}"#;
5431        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5432        let loc = sm.original_position_for(0, 0).unwrap();
5433        assert_eq!(loc.name, Some(0));
5434    }
5435
5436    #[test]
5437    fn from_parts_with_line_gap() {
5438        // Mappings with a gap between lines to exercise line_offsets forward fill
5439        let sm = SourceMap::from_parts(
5440            None,
5441            None,
5442            vec!["a.js".to_string()],
5443            vec![None],
5444            vec![],
5445            vec![
5446                Mapping {
5447                    generated_line: 0,
5448                    generated_column: 0,
5449                    source: 0,
5450                    original_line: 0,
5451                    original_column: 0,
5452                    name: NO_NAME,
5453                    is_range_mapping: false,
5454                },
5455                Mapping {
5456                    generated_line: 5,
5457                    generated_column: 0,
5458                    source: 0,
5459                    original_line: 5,
5460                    original_column: 0,
5461                    name: NO_NAME,
5462                    is_range_mapping: false,
5463                },
5464            ],
5465            vec![],
5466            None,
5467            None,
5468        );
5469        assert!(sm.original_position_for(0, 0).is_some());
5470        assert!(sm.original_position_for(5, 0).is_some());
5471        // Lines 1-4 have no mappings
5472        assert!(sm.original_position_for(1, 0).is_none());
5473    }
5474
5475    #[test]
5476    fn lazy_decode_line_with_names_and_generated_only() {
5477        // LazySourceMap with both named and generated-only segments
5478        let json = r#"{"version":3,"sources":["a.js"],"names":["fn"],"mappings":"A,AAAAC"}"#;
5479        let sm = LazySourceMap::from_json(json).unwrap();
5480        let line = sm.decode_line(0).unwrap();
5481        assert!(line.len() >= 2);
5482        // First is generated-only
5483        assert_eq!(line[0].source, NO_SOURCE);
5484        // Second has name
5485        assert_ne!(line[1].name, NO_NAME);
5486    }
5487
5488    #[test]
5489    fn generated_position_glb_source_mismatch() {
5490        // a.js maps at (0,0)->(0,0), b.js maps at (0,5)->(1,0)
5491        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5492        let sm = SourceMap::from_json(json).unwrap();
5493
5494        // LUB for source that exists but position is way beyond all mappings
5495        let loc = sm.generated_position_for_with_bias("a.js", 100, 0, Bias::LeastUpperBound);
5496        assert!(loc.is_none());
5497
5498        // GLB for position before the only mapping in b.js (b.js has mapping at original 1,0)
5499        // Searching for (0,0) in b.js: partition_point finds first >= target,
5500        // then idx-1 if not exact, but that idx-1 maps to a.js (source mismatch), so None
5501        let loc = sm.generated_position_for_with_bias("b.js", 0, 0, Bias::GreatestLowerBound);
5502        assert!(loc.is_none());
5503
5504        // GLB for exact position in b.js
5505        let loc = sm.generated_position_for_with_bias("b.js", 1, 0, Bias::GreatestLowerBound);
5506        assert!(loc.is_some());
5507
5508        // LUB source mismatch: search for position in b.js that lands on a.js mapping
5509        let loc = sm.generated_position_for_with_bias("b.js", 99, 0, Bias::LeastUpperBound);
5510        assert!(loc.is_none());
5511    }
5512
5513    // ── Coverage gap tests ───────────────────────────────────────────
5514
5515    #[test]
5516    fn from_json_invalid_scopes_error() {
5517        // Invalid scopes string to trigger ParseError::Scopes
5518        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"!!invalid!!"}"#;
5519        let err = SourceMap::from_json(json).unwrap_err();
5520        assert!(matches!(err, ParseError::Scopes(_)));
5521    }
5522
5523    #[test]
5524    fn lazy_from_json_invalid_scopes_error() {
5525        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"!!invalid!!"}"#;
5526        let err = LazySourceMap::from_json(json).unwrap_err();
5527        assert!(matches!(err, ParseError::Scopes(_)));
5528    }
5529
5530    #[test]
5531    fn from_json_lines_invalid_scopes_error() {
5532        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"!!invalid!!"}"#;
5533        let err = SourceMap::from_json_lines(json, 0, 1).unwrap_err();
5534        assert!(matches!(err, ParseError::Scopes(_)));
5535    }
5536
5537    #[test]
5538    fn from_json_lines_invalid_version() {
5539        let json = r#"{"version":2,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5540        let err = SourceMap::from_json_lines(json, 0, 1).unwrap_err();
5541        assert!(matches!(err, ParseError::InvalidVersion(2)));
5542    }
5543
5544    #[test]
5545    fn indexed_map_with_ignore_list_remapped() {
5546        // Indexed map with 2 sections that have overlapping ignore_list
5547        let json = r#"{
5548            "version": 3,
5549            "sections": [{
5550                "offset": {"line": 0, "column": 0},
5551                "map": {
5552                    "version": 3,
5553                    "sources": ["a.js", "b.js"],
5554                    "names": [],
5555                    "mappings": "AAAA;ACAA",
5556                    "ignoreList": [1]
5557                }
5558            }, {
5559                "offset": {"line": 5, "column": 0},
5560                "map": {
5561                    "version": 3,
5562                    "sources": ["b.js", "c.js"],
5563                    "names": [],
5564                    "mappings": "AAAA;ACAA",
5565                    "ignoreList": [0]
5566                }
5567            }]
5568        }"#;
5569        let sm = SourceMap::from_json(json).unwrap();
5570        // b.js should be deduped across sections, ignore_list should have b.js global index
5571        assert!(!sm.ignore_list.is_empty());
5572    }
5573
5574    #[test]
5575    fn to_json_with_debug_id() {
5576        let sm = SourceMap::from_parts(
5577            Some("out.js".to_string()),
5578            None,
5579            vec!["a.js".to_string()],
5580            vec![None],
5581            vec![],
5582            vec![Mapping {
5583                generated_line: 0,
5584                generated_column: 0,
5585                source: 0,
5586                original_line: 0,
5587                original_column: 0,
5588                name: NO_NAME,
5589                is_range_mapping: false,
5590            }],
5591            vec![],
5592            Some("abc-123".to_string()),
5593            None,
5594        );
5595        let json = sm.to_json();
5596        assert!(json.contains(r#""debugId":"abc-123""#));
5597    }
5598
5599    #[test]
5600    fn to_json_with_ignore_list_and_extensions() {
5601        let mut sm = SourceMap::from_parts(
5602            None,
5603            None,
5604            vec!["a.js".to_string(), "b.js".to_string()],
5605            vec![None, None],
5606            vec![],
5607            vec![Mapping {
5608                generated_line: 0,
5609                generated_column: 0,
5610                source: 0,
5611                original_line: 0,
5612                original_column: 0,
5613                name: NO_NAME,
5614                is_range_mapping: false,
5615            }],
5616            vec![1],
5617            None,
5618            None,
5619        );
5620        sm.extensions
5621            .insert("x_test".to_string(), serde_json::json!(42));
5622        let json = sm.to_json();
5623        assert!(json.contains("\"ignoreList\":[1]"));
5624        assert!(json.contains("\"x_test\":42"));
5625    }
5626
5627    #[test]
5628    fn from_vlq_with_all_options() {
5629        let sm = SourceMap::from_vlq(
5630            "AAAA;AACA",
5631            vec!["a.js".to_string()],
5632            vec![],
5633            Some("out.js".to_string()),
5634            Some("src/".to_string()),
5635            vec![Some("content".to_string())],
5636            vec![0],
5637            Some("debug-123".to_string()),
5638        )
5639        .unwrap();
5640        assert_eq!(sm.source(0), "a.js");
5641        assert!(sm.original_position_for(0, 0).is_some());
5642        assert!(sm.original_position_for(1, 0).is_some());
5643    }
5644
5645    #[test]
5646    fn lazy_into_sourcemap_roundtrip() {
5647        let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA;AACAA"}"#;
5648        let lazy = LazySourceMap::from_json(json).unwrap();
5649        let sm = lazy.into_sourcemap().unwrap();
5650        assert!(sm.original_position_for(0, 0).is_some());
5651        assert!(sm.original_position_for(1, 0).is_some());
5652        assert_eq!(sm.name(0), "x");
5653    }
5654
5655    #[test]
5656    fn lazy_original_position_for_no_match() {
5657        // LazySourceMap: column before any mapping should return None (Err(0) branch)
5658        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"KAAA"}"#;
5659        let sm = LazySourceMap::from_json(json).unwrap();
5660        // Column 0 is before column 5 (K = 5), should return None
5661        assert!(sm.original_position_for(0, 0).is_none());
5662    }
5663
5664    #[test]
5665    fn lazy_original_position_for_empty_line() {
5666        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":";AAAA"}"#;
5667        let sm = LazySourceMap::from_json(json).unwrap();
5668        // Line 0 is empty
5669        assert!(sm.original_position_for(0, 0).is_none());
5670        // Line 1 has mapping
5671        assert!(sm.original_position_for(1, 0).is_some());
5672    }
5673
5674    #[test]
5675    fn lazy_original_position_generated_only() {
5676        // Only a 1-field (generated-only) segment on line 0
5677        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A;AAAA"}"#;
5678        let sm = LazySourceMap::from_json(json).unwrap();
5679        // Line 0 has only generated-only segment → returns None
5680        assert!(sm.original_position_for(0, 0).is_none());
5681        // Line 1 has a 4-field segment → returns Some
5682        assert!(sm.original_position_for(1, 0).is_some());
5683    }
5684
5685    #[test]
5686    fn from_json_lines_null_source() {
5687        let json = r#"{"version":3,"sources":[null,"a.js"],"names":[],"mappings":"ACAA"}"#;
5688        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5689        assert!(sm.mapping_count() >= 1);
5690    }
5691
5692    #[test]
5693    fn from_json_lines_with_source_root_prefix() {
5694        let json =
5695            r#"{"version":3,"sourceRoot":"lib/","sources":["b.js"],"names":[],"mappings":"AAAA"}"#;
5696        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5697        assert_eq!(sm.source(0), "lib/b.js");
5698    }
5699
5700    #[test]
5701    fn generated_position_for_glb_idx_zero() {
5702        // When the reverse index partition_point returns 0, GLB should return None
5703        // Create a map where source "a.js" only has mapping at original (5,0)
5704        // Searching for (0,0) in GLB mode: partition_point returns 0 (nothing <= (0,0)), so None
5705        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAKA"}"#;
5706        let sm = SourceMap::from_json(json).unwrap();
5707        let loc = sm.generated_position_for_with_bias("a.js", 0, 0, Bias::GreatestLowerBound);
5708        assert!(loc.is_none());
5709    }
5710
5711    #[test]
5712    fn from_json_lines_with_ignore_list() {
5713        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
5714        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5715        assert_eq!(sm.ignore_list, vec![1]);
5716    }
5717
5718    #[test]
5719    fn validate_deep_out_of_order_mappings() {
5720        // Manually construct a map with out-of-order segments
5721        let sm = SourceMap::from_parts(
5722            None,
5723            None,
5724            vec!["a.js".to_string()],
5725            vec![None],
5726            vec![],
5727            vec![
5728                Mapping {
5729                    generated_line: 1,
5730                    generated_column: 0,
5731                    source: 0,
5732                    original_line: 0,
5733                    original_column: 0,
5734                    name: NO_NAME,
5735                    is_range_mapping: false,
5736                },
5737                Mapping {
5738                    generated_line: 0,
5739                    generated_column: 0,
5740                    source: 0,
5741                    original_line: 0,
5742                    original_column: 0,
5743                    name: NO_NAME,
5744                    is_range_mapping: false,
5745                },
5746            ],
5747            vec![],
5748            None,
5749            None,
5750        );
5751        let warnings = validate_deep(&sm);
5752        assert!(warnings.iter().any(|w| w.contains("out of order")));
5753    }
5754
5755    #[test]
5756    fn validate_deep_out_of_bounds_source() {
5757        let sm = SourceMap::from_parts(
5758            None,
5759            None,
5760            vec!["a.js".to_string()],
5761            vec![None],
5762            vec![],
5763            vec![Mapping {
5764                generated_line: 0,
5765                generated_column: 0,
5766                source: 5,
5767                original_line: 0,
5768                original_column: 0,
5769                name: NO_NAME,
5770                is_range_mapping: false,
5771            }],
5772            vec![],
5773            None,
5774            None,
5775        );
5776        let warnings = validate_deep(&sm);
5777        assert!(
5778            warnings
5779                .iter()
5780                .any(|w| w.contains("source index") && w.contains("out of bounds"))
5781        );
5782    }
5783
5784    #[test]
5785    fn validate_deep_out_of_bounds_name() {
5786        let sm = SourceMap::from_parts(
5787            None,
5788            None,
5789            vec!["a.js".to_string()],
5790            vec![None],
5791            vec!["foo".to_string()],
5792            vec![Mapping {
5793                generated_line: 0,
5794                generated_column: 0,
5795                source: 0,
5796                original_line: 0,
5797                original_column: 0,
5798                name: 5,
5799                is_range_mapping: false,
5800            }],
5801            vec![],
5802            None,
5803            None,
5804        );
5805        let warnings = validate_deep(&sm);
5806        assert!(
5807            warnings
5808                .iter()
5809                .any(|w| w.contains("name index") && w.contains("out of bounds"))
5810        );
5811    }
5812
5813    #[test]
5814    fn validate_deep_out_of_bounds_ignore_list() {
5815        let sm = SourceMap::from_parts(
5816            None,
5817            None,
5818            vec!["a.js".to_string()],
5819            vec![None],
5820            vec![],
5821            vec![Mapping {
5822                generated_line: 0,
5823                generated_column: 0,
5824                source: 0,
5825                original_line: 0,
5826                original_column: 0,
5827                name: NO_NAME,
5828                is_range_mapping: false,
5829            }],
5830            vec![10],
5831            None,
5832            None,
5833        );
5834        let warnings = validate_deep(&sm);
5835        assert!(
5836            warnings
5837                .iter()
5838                .any(|w| w.contains("ignoreList") && w.contains("out of bounds"))
5839        );
5840    }
5841
5842    #[test]
5843    fn source_mapping_url_inline_decoded() {
5844        // Test that inline data URIs actually decode base64 and return the parsed map
5845        let map_json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5846        let encoded = base64_encode_simple(map_json);
5847        let input = format!("var x;\n//# sourceMappingURL=data:application/json;base64,{encoded}");
5848        let url = parse_source_mapping_url(&input);
5849        match url {
5850            Some(SourceMappingUrl::Inline(json)) => {
5851                assert!(json.contains("version"));
5852                assert!(json.contains("AAAA"));
5853            }
5854            _ => panic!("expected inline source map"),
5855        }
5856    }
5857
5858    #[test]
5859    fn source_mapping_url_charset_variant() {
5860        let map_json = r#"{"version":3}"#;
5861        let encoded = base64_encode_simple(map_json);
5862        let input =
5863            format!("x\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,{encoded}");
5864        let url = parse_source_mapping_url(&input);
5865        assert!(matches!(url, Some(SourceMappingUrl::Inline(_))));
5866    }
5867
5868    #[test]
5869    fn source_mapping_url_invalid_base64_falls_through_to_external() {
5870        // Data URI with invalid base64 that fails to decode should still return External
5871        let input = "x\n//# sourceMappingURL=data:application/json;base64,!!!invalid!!!";
5872        let url = parse_source_mapping_url(input);
5873        // Invalid base64 → base64_decode returns None → falls through to External
5874        assert!(matches!(url, Some(SourceMappingUrl::External(_))));
5875    }
5876
5877    #[test]
5878    fn from_json_lines_with_extensions_preserved() {
5879        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom":99}"#;
5880        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5881        assert!(sm.extensions.contains_key("x_custom"));
5882    }
5883
5884    // Helper for base64 encoding in tests
5885    fn base64_encode_simple(input: &str) -> String {
5886        const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5887        let bytes = input.as_bytes();
5888        let mut result = String::new();
5889        for chunk in bytes.chunks(3) {
5890            let b0 = chunk[0] as u32;
5891            let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
5892            let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
5893            let n = (b0 << 16) | (b1 << 8) | b2;
5894            result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
5895            result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
5896            if chunk.len() > 1 {
5897                result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
5898            } else {
5899                result.push('=');
5900            }
5901            if chunk.len() > 2 {
5902                result.push(CHARS[(n & 0x3F) as usize] as char);
5903            } else {
5904                result.push('=');
5905            }
5906        }
5907        result
5908    }
5909
5910    // ── MappingsIter tests ──────────────────────────────────────
5911
5912    #[test]
5913    fn mappings_iter_matches_decode() {
5914        let vlq = "AAAA;AACA,EAAA;AACA";
5915        let iter_mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
5916        let (decoded, _) = decode_mappings(vlq).unwrap();
5917        assert_eq!(iter_mappings.len(), decoded.len());
5918        for (a, b) in iter_mappings.iter().zip(decoded.iter()) {
5919            assert_eq!(a.generated_line, b.generated_line);
5920            assert_eq!(a.generated_column, b.generated_column);
5921            assert_eq!(a.source, b.source);
5922            assert_eq!(a.original_line, b.original_line);
5923            assert_eq!(a.original_column, b.original_column);
5924            assert_eq!(a.name, b.name);
5925        }
5926    }
5927
5928    #[test]
5929    fn mappings_iter_empty() {
5930        let mappings: Vec<Mapping> = MappingsIter::new("").collect::<Result<_, _>>().unwrap();
5931        assert!(mappings.is_empty());
5932    }
5933
5934    #[test]
5935    fn mappings_iter_generated_only() {
5936        let vlq = "A,AAAA";
5937        let mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
5938        assert_eq!(mappings.len(), 2);
5939        assert_eq!(mappings[0].source, u32::MAX);
5940        assert_eq!(mappings[1].source, 0);
5941    }
5942
5943    #[test]
5944    fn mappings_iter_with_names() {
5945        let vlq = "AAAAA";
5946        let mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
5947        assert_eq!(mappings.len(), 1);
5948        assert_eq!(mappings[0].name, 0);
5949    }
5950
5951    #[test]
5952    fn mappings_iter_multiple_lines() {
5953        let vlq = "AAAA;AACA;AACA";
5954        let mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
5955        assert_eq!(mappings.len(), 3);
5956        assert_eq!(mappings[0].generated_line, 0);
5957        assert_eq!(mappings[1].generated_line, 1);
5958        assert_eq!(mappings[2].generated_line, 2);
5959    }
5960    // ── Range mappings tests ──────────────────────────────────────
5961
5962    #[test]
5963    fn range_mappings_basic_decode() {
5964        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC,GAAG","rangeMappings":"A,C"}"#;
5965        let sm = SourceMap::from_json(json).unwrap();
5966        assert!(sm.all_mappings()[0].is_range_mapping);
5967        assert!(!sm.all_mappings()[1].is_range_mapping);
5968        assert!(sm.all_mappings()[2].is_range_mapping);
5969    }
5970
5971    #[test]
5972    fn range_mapping_lookup_with_delta() {
5973        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,GAAG","rangeMappings":"A"}"#;
5974        let sm = SourceMap::from_json(json).unwrap();
5975        assert_eq!(sm.original_position_for(0, 0).unwrap().column, 0);
5976        assert_eq!(sm.original_position_for(0, 1).unwrap().column, 1);
5977        assert_eq!(sm.original_position_for(0, 2).unwrap().column, 2);
5978        assert_eq!(sm.original_position_for(0, 3).unwrap().column, 3);
5979    }
5980
5981    #[test]
5982    fn range_mapping_cross_line() {
5983        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA","rangeMappings":"A"}"#;
5984        let sm = SourceMap::from_json(json).unwrap();
5985        assert_eq!(sm.original_position_for(1, 5).unwrap().line, 1);
5986        assert_eq!(sm.original_position_for(1, 5).unwrap().column, 0);
5987        assert_eq!(sm.original_position_for(2, 10).unwrap().line, 2);
5988    }
5989
5990    #[test]
5991    fn range_mapping_encode_roundtrip() {
5992        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC,GAAG","rangeMappings":"A,C"}"#;
5993        assert_eq!(
5994            SourceMap::from_json(json)
5995                .unwrap()
5996                .encode_range_mappings()
5997                .unwrap(),
5998            "A,C"
5999        );
6000    }
6001
6002    #[test]
6003    fn no_range_mappings_test() {
6004        let sm = SourceMap::from_json(
6005            r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA"}"#,
6006        )
6007        .unwrap();
6008        assert!(!sm.has_range_mappings());
6009        assert!(sm.encode_range_mappings().is_none());
6010    }
6011
6012    #[test]
6013    fn range_mappings_multi_line_test() {
6014        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC;AAAA","rangeMappings":"A;A"}"#).unwrap();
6015        assert!(sm.all_mappings()[0].is_range_mapping);
6016        assert!(!sm.all_mappings()[1].is_range_mapping);
6017        assert!(sm.all_mappings()[2].is_range_mapping);
6018    }
6019
6020    #[test]
6021    fn range_mappings_json_roundtrip() {
6022        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC,GAAG","rangeMappings":"A,C"}"#).unwrap();
6023        let output = sm.to_json();
6024        assert!(output.contains("rangeMappings"));
6025        assert_eq!(
6026            SourceMap::from_json(&output).unwrap().range_mapping_count(),
6027            2
6028        );
6029    }
6030
6031    #[test]
6032    fn range_mappings_absent_from_json_test() {
6033        assert!(
6034            !SourceMap::from_json(
6035                r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA"}"#
6036            )
6037            .unwrap()
6038            .to_json()
6039            .contains("rangeMappings")
6040        );
6041    }
6042
6043    #[test]
6044    fn range_mapping_fallback_test() {
6045        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;KACK","rangeMappings":"A"}"#).unwrap();
6046        let loc = sm.original_position_for(1, 2).unwrap();
6047        assert_eq!(loc.line, 1);
6048        assert_eq!(loc.column, 0);
6049    }
6050
6051    #[test]
6052    fn range_mapping_no_fallback_non_range() {
6053        assert!(
6054            SourceMap::from_json(
6055                r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA"}"#
6056            )
6057            .unwrap()
6058            .original_position_for(1, 5)
6059            .is_none()
6060        );
6061    }
6062
6063    #[test]
6064    fn range_mapping_from_vlq_test() {
6065        let sm = SourceMap::from_vlq_with_range_mappings(
6066            "AAAA,CAAC",
6067            vec!["input.js".into()],
6068            vec![],
6069            None,
6070            None,
6071            vec![],
6072            vec![],
6073            None,
6074            Some("A"),
6075        )
6076        .unwrap();
6077        assert!(sm.all_mappings()[0].is_range_mapping);
6078        assert!(!sm.all_mappings()[1].is_range_mapping);
6079    }
6080
6081    #[test]
6082    fn range_mapping_encode_multi_line_test() {
6083        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC;AAAA,CAAC","rangeMappings":"A;B"}"#).unwrap();
6084        assert!(sm.all_mappings()[0].is_range_mapping);
6085        assert!(!sm.all_mappings()[1].is_range_mapping);
6086        assert!(!sm.all_mappings()[2].is_range_mapping);
6087        assert!(sm.all_mappings()[3].is_range_mapping);
6088        assert_eq!(sm.encode_range_mappings().unwrap(), "A;B");
6089    }
6090
6091    #[test]
6092    fn range_mapping_from_parts_test() {
6093        let sm = SourceMap::from_parts(
6094            None,
6095            None,
6096            vec!["input.js".into()],
6097            vec![],
6098            vec![],
6099            vec![
6100                Mapping {
6101                    generated_line: 0,
6102                    generated_column: 0,
6103                    source: 0,
6104                    original_line: 0,
6105                    original_column: 0,
6106                    name: NO_NAME,
6107                    is_range_mapping: true,
6108                },
6109                Mapping {
6110                    generated_line: 0,
6111                    generated_column: 5,
6112                    source: 0,
6113                    original_line: 0,
6114                    original_column: 5,
6115                    name: NO_NAME,
6116                    is_range_mapping: false,
6117                },
6118            ],
6119            vec![],
6120            None,
6121            None,
6122        );
6123        assert_eq!(sm.original_position_for(0, 2).unwrap().column, 2);
6124        assert_eq!(sm.original_position_for(0, 6).unwrap().column, 5);
6125    }
6126
6127    #[test]
6128    fn range_mapping_indexed_test() {
6129        let sm = SourceMap::from_json(r#"{"version":3,"sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","rangeMappings":"A"}}]}"#).unwrap();
6130        assert!(sm.has_range_mappings());
6131        assert_eq!(sm.original_position_for(1, 3).unwrap().line, 1);
6132    }
6133
6134    #[test]
6135    fn range_mapping_empty_string_test() {
6136        assert!(!SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA","rangeMappings":""}"#).unwrap().has_range_mappings());
6137    }
6138
6139    #[test]
6140    fn range_mapping_lub_no_underflow() {
6141        // Range mapping at col 5, query col 2 with LUB bias
6142        // LUB should find the mapping at col 5, but NOT apply range delta
6143        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"KAAK","rangeMappings":"A"}"#;
6144        let sm = SourceMap::from_json(json).unwrap();
6145
6146        let loc = sm.original_position_for_with_bias(0, 2, Bias::LeastUpperBound);
6147        assert!(loc.is_some());
6148        let loc = loc.unwrap();
6149        // Should return the mapping's own position, not apply a delta
6150        assert_eq!(loc.line, 0);
6151        assert_eq!(loc.column, 5);
6152    }
6153
6154    // ── Builder tests ──────────────────────────────────────────────
6155
6156    #[test]
6157    fn builder_basic() {
6158        let sm = SourceMap::builder()
6159            .file("output.js")
6160            .sources(["input.ts"])
6161            .sources_content([Some("let x = 1;")])
6162            .names(["x"])
6163            .mappings([Mapping {
6164                generated_line: 0,
6165                generated_column: 0,
6166                source: 0,
6167                original_line: 0,
6168                original_column: 4,
6169                name: 0,
6170                is_range_mapping: false,
6171            }])
6172            .build();
6173
6174        assert_eq!(sm.file.as_deref(), Some("output.js"));
6175        assert_eq!(sm.sources, vec!["input.ts"]);
6176        assert_eq!(sm.sources_content, vec![Some("let x = 1;".to_string())]);
6177        assert_eq!(sm.names, vec!["x"]);
6178        assert_eq!(sm.mapping_count(), 1);
6179
6180        let loc = sm.original_position_for(0, 0).unwrap();
6181        assert_eq!(sm.source(loc.source), "input.ts");
6182        assert_eq!(loc.column, 4);
6183        assert_eq!(sm.name(loc.name.unwrap()), "x");
6184    }
6185
6186    #[test]
6187    fn builder_empty() {
6188        let sm = SourceMap::builder().build();
6189        assert_eq!(sm.mapping_count(), 0);
6190        assert_eq!(sm.sources.len(), 0);
6191        assert_eq!(sm.names.len(), 0);
6192        assert!(sm.file.is_none());
6193    }
6194
6195    #[test]
6196    fn builder_multiple_sources() {
6197        let sm = SourceMap::builder()
6198            .sources(["a.ts", "b.ts", "c.ts"])
6199            .sources_content([Some("// a"), Some("// b"), None])
6200            .mappings([
6201                Mapping {
6202                    generated_line: 0,
6203                    generated_column: 0,
6204                    source: 0,
6205                    original_line: 0,
6206                    original_column: 0,
6207                    name: u32::MAX,
6208                    is_range_mapping: false,
6209                },
6210                Mapping {
6211                    generated_line: 1,
6212                    generated_column: 0,
6213                    source: 1,
6214                    original_line: 0,
6215                    original_column: 0,
6216                    name: u32::MAX,
6217                    is_range_mapping: false,
6218                },
6219                Mapping {
6220                    generated_line: 2,
6221                    generated_column: 0,
6222                    source: 2,
6223                    original_line: 0,
6224                    original_column: 0,
6225                    name: u32::MAX,
6226                    is_range_mapping: false,
6227                },
6228            ])
6229            .build();
6230
6231        assert_eq!(sm.sources.len(), 3);
6232        assert_eq!(sm.mapping_count(), 3);
6233        assert_eq!(sm.line_count(), 3);
6234
6235        let loc0 = sm.original_position_for(0, 0).unwrap();
6236        assert_eq!(sm.source(loc0.source), "a.ts");
6237
6238        let loc1 = sm.original_position_for(1, 0).unwrap();
6239        assert_eq!(sm.source(loc1.source), "b.ts");
6240
6241        let loc2 = sm.original_position_for(2, 0).unwrap();
6242        assert_eq!(sm.source(loc2.source), "c.ts");
6243    }
6244
6245    #[test]
6246    fn builder_with_iterators() {
6247        let source_names: Vec<String> = (0..5).map(|i| format!("mod_{i}.ts")).collect();
6248        let mappings = (0..5u32).map(|i| Mapping {
6249            generated_line: i,
6250            generated_column: 0,
6251            source: i,
6252            original_line: i,
6253            original_column: 0,
6254            name: u32::MAX,
6255            is_range_mapping: false,
6256        });
6257
6258        let sm = SourceMap::builder()
6259            .sources(source_names.iter().map(|s| s.as_str()))
6260            .mappings(mappings)
6261            .build();
6262
6263        assert_eq!(sm.sources.len(), 5);
6264        assert_eq!(sm.mapping_count(), 5);
6265        for i in 0..5u32 {
6266            let loc = sm.original_position_for(i, 0).unwrap();
6267            assert_eq!(sm.source(loc.source), format!("mod_{i}.ts"));
6268        }
6269    }
6270
6271    #[test]
6272    fn builder_ignore_list_and_debug_id() {
6273        let sm = SourceMap::builder()
6274            .sources(["app.ts", "node_modules/lib.js"])
6275            .ignore_list([1])
6276            .debug_id("85314830-023f-4cf1-a267-535f4e37bb17")
6277            .build();
6278
6279        assert_eq!(sm.ignore_list, vec![1]);
6280        assert_eq!(
6281            sm.debug_id.as_deref(),
6282            Some("85314830-023f-4cf1-a267-535f4e37bb17")
6283        );
6284    }
6285
6286    #[test]
6287    fn builder_range_mappings() {
6288        let sm = SourceMap::builder()
6289            .sources(["input.ts"])
6290            .mappings([
6291                Mapping {
6292                    generated_line: 0,
6293                    generated_column: 0,
6294                    source: 0,
6295                    original_line: 0,
6296                    original_column: 0,
6297                    name: u32::MAX,
6298                    is_range_mapping: true,
6299                },
6300                Mapping {
6301                    generated_line: 0,
6302                    generated_column: 10,
6303                    source: 0,
6304                    original_line: 5,
6305                    original_column: 0,
6306                    name: u32::MAX,
6307                    is_range_mapping: false,
6308                },
6309            ])
6310            .build();
6311
6312        assert!(sm.has_range_mappings());
6313        assert_eq!(sm.mapping_count(), 2);
6314    }
6315
6316    #[test]
6317    fn builder_json_roundtrip() {
6318        let sm = SourceMap::builder()
6319            .file("out.js")
6320            .source_root("/src/")
6321            .sources(["a.ts", "b.ts"])
6322            .sources_content([Some("// a"), Some("// b")])
6323            .names(["foo", "bar"])
6324            .mappings([
6325                Mapping {
6326                    generated_line: 0,
6327                    generated_column: 0,
6328                    source: 0,
6329                    original_line: 0,
6330                    original_column: 0,
6331                    name: 0,
6332                    is_range_mapping: false,
6333                },
6334                Mapping {
6335                    generated_line: 1,
6336                    generated_column: 5,
6337                    source: 1,
6338                    original_line: 3,
6339                    original_column: 2,
6340                    name: 1,
6341                    is_range_mapping: false,
6342                },
6343            ])
6344            .build();
6345
6346        let json = sm.to_json();
6347        let sm2 = SourceMap::from_json(&json).unwrap();
6348
6349        assert_eq!(sm2.file, sm.file);
6350        // source_root is prepended to sources on parse
6351        assert_eq!(sm2.sources, vec!["/src/a.ts", "/src/b.ts"]);
6352        assert_eq!(sm2.names, sm.names);
6353        assert_eq!(sm2.mapping_count(), sm.mapping_count());
6354
6355        for m in sm.all_mappings() {
6356            let a = sm.original_position_for(m.generated_line, m.generated_column);
6357            let b = sm2.original_position_for(m.generated_line, m.generated_column);
6358            match (a, b) {
6359                (Some(a), Some(b)) => {
6360                    assert_eq!(a.source, b.source);
6361                    assert_eq!(a.line, b.line);
6362                    assert_eq!(a.column, b.column);
6363                    assert_eq!(a.name, b.name);
6364                }
6365                (None, None) => {}
6366                _ => panic!("lookup mismatch"),
6367            }
6368        }
6369    }
6370}