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