Skip to main content

srcmap_generator/
lib.rs

1//! High-performance source map generator (ECMA-426).
2//!
3//! Builds source maps incrementally by adding mappings one at a time.
4//! Outputs standard source map v3 JSON.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use srcmap_generator::SourceMapGenerator;
10//!
11//! fn main() {
12//!     let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
13//!
14//!     let src = builder.add_source("src/app.ts");
15//!     builder.set_source_content(src, "const x = 1;".to_string());
16//!
17//!     let name = builder.add_name("x");
18//!     builder.add_named_mapping(0, 0, src, 0, 6, name);
19//!     builder.add_mapping(1, 0, src, 1, 0);
20//!
21//!     let json = builder.to_json();
22//!     assert!(json.contains(r#""version":3"#));
23//!     assert!(json.contains(r#""sources":["src/app.ts"]"#));
24//! }
25//! ```
26
27use rustc_hash::FxHashMap;
28use srcmap_codec::{vlq_encode, vlq_encode_unchecked, vlq_encode_unsigned};
29use srcmap_scopes::ScopeInfo;
30
31// ── Public types ───────────────────────────────────────────────────
32
33/// A mapping from a generated position to an original position.
34///
35/// Used with [`SourceMapGenerator`] to define position relationships.
36/// All positions are 0-based.
37#[derive(Debug, Clone)]
38pub struct Mapping {
39    /// 0-based line in the generated output.
40    pub generated_line: u32,
41    /// 0-based column in the generated output.
42    pub generated_column: u32,
43    /// Source index from [`SourceMapGenerator::add_source`], or `None` for generated-only.
44    pub source: Option<u32>,
45    /// 0-based line in the original source.
46    pub original_line: u32,
47    /// 0-based column in the original source.
48    pub original_column: u32,
49    /// Name index from [`SourceMapGenerator::add_name`], or `None`.
50    pub name: Option<u32>,
51    /// Whether this mapping is a range mapping (ECMA-426 rangeMappings proposal).
52    pub is_range_mapping: bool,
53}
54
55/// Builder for creating source maps incrementally.
56///
57/// Register sources and names first (they return indices), then add mappings
58/// that reference those indices. Mappings should be added in generated-position
59/// order, though the builder does not require it.
60///
61/// Sources and names are automatically deduplicated.
62///
63/// # Workflow
64///
65/// 1. [`add_source`](Self::add_source) — register each original file
66/// 2. [`set_source_content`](Self::set_source_content) — optionally attach content
67/// 3. [`add_name`](Self::add_name) — register identifier names
68/// 4. [`add_mapping`](Self::add_mapping) / [`add_named_mapping`](Self::add_named_mapping) — add mappings
69/// 5. [`to_json`](Self::to_json) — serialize to JSON
70#[derive(Debug)]
71pub struct SourceMapGenerator {
72    file: Option<String>,
73    source_root: Option<String>,
74    sources: Vec<String>,
75    sources_content: Vec<Option<String>>,
76    names: Vec<String>,
77    mappings: Vec<Mapping>,
78    ignore_list: Vec<u32>,
79    debug_id: Option<String>,
80    scopes: Option<ScopeInfo>,
81    assume_sorted: bool,
82
83    // Dedup maps for O(1) lookup
84    source_map: FxHashMap<String, u32>,
85    name_map: FxHashMap<String, u32>,
86}
87
88impl SourceMapGenerator {
89    /// Create a new empty source map generator.
90    pub fn new(file: Option<String>) -> Self {
91        Self {
92            file,
93            source_root: None,
94            sources: Vec::new(),
95            sources_content: Vec::new(),
96            names: Vec::new(),
97            mappings: Vec::new(),
98            ignore_list: Vec::new(),
99            debug_id: None,
100            scopes: None,
101            assume_sorted: false,
102            source_map: FxHashMap::default(),
103            name_map: FxHashMap::default(),
104        }
105    }
106
107    /// Set the source root prefix.
108    pub fn set_source_root(&mut self, root: impl Into<String>) {
109        self.source_root = Some(root.into());
110    }
111
112    /// Set the debug ID (UUID) for this source map (ECMA-426).
113    pub fn set_debug_id(&mut self, id: impl Into<String>) {
114        self.debug_id = Some(id.into());
115    }
116
117    /// Set scope and variable information (ECMA-426 scopes proposal).
118    pub fn set_scopes(&mut self, scopes: ScopeInfo) {
119        self.scopes = Some(scopes);
120    }
121
122    /// Assume mappings are already sorted by `(generated_line, generated_column)`.
123    ///
124    /// When set to `true`, [`encode_mappings`](Self::encode_mappings) and
125    /// [`to_decoded_map`](Self::to_decoded_map) skip the sort step, avoiding
126    /// both the `O(n log n)` sort and the `Vec<&Mapping>` allocation.
127    /// Most bundlers add mappings in order, so this is a safe optimization
128    /// in the common case.
129    pub fn set_assume_sorted(&mut self, sorted: bool) {
130        self.assume_sorted = sorted;
131    }
132
133    /// Register a source file and return its index.
134    #[inline]
135    pub fn add_source(&mut self, source: &str) -> u32 {
136        if let Some(&idx) = self.source_map.get(source) {
137            return idx;
138        }
139        let idx = self.sources.len() as u32;
140        self.sources.push(source.to_string());
141        self.sources_content.push(None);
142        self.source_map.insert(source.to_string(), idx);
143        idx
144    }
145
146    /// Set the content for a source file.
147    pub fn set_source_content(&mut self, source_idx: u32, content: impl Into<String>) {
148        if (source_idx as usize) < self.sources_content.len() {
149            self.sources_content[source_idx as usize] = Some(content.into());
150        }
151    }
152
153    /// Register a name and return its index.
154    #[inline]
155    pub fn add_name(&mut self, name: &str) -> u32 {
156        if let Some(&idx) = self.name_map.get(name) {
157            return idx;
158        }
159        let idx = self.names.len() as u32;
160        self.names.push(name.to_string());
161        self.name_map.insert(name.to_string(), idx);
162        idx
163    }
164
165    /// Add a source index to the ignore list.
166    pub fn add_to_ignore_list(&mut self, source_idx: u32) {
167        if !self.ignore_list.contains(&source_idx) {
168            self.ignore_list.push(source_idx);
169        }
170    }
171
172    /// Add a mapping with no source information (generated-only).
173    pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
174        self.mappings.push(Mapping {
175            generated_line,
176            generated_column,
177            source: None,
178            original_line: 0,
179            original_column: 0,
180            name: None,
181            is_range_mapping: false,
182        });
183    }
184
185    /// Add a mapping from generated position to original position.
186    pub fn add_mapping(
187        &mut self,
188        generated_line: u32,
189        generated_column: u32,
190        source: u32,
191        original_line: u32,
192        original_column: u32,
193    ) {
194        self.mappings.push(Mapping {
195            generated_line,
196            generated_column,
197            source: Some(source),
198            original_line,
199            original_column,
200            name: None,
201            is_range_mapping: false,
202        });
203    }
204
205    /// Add a mapping with a name.
206    pub fn add_named_mapping(
207        &mut self,
208        generated_line: u32,
209        generated_column: u32,
210        source: u32,
211        original_line: u32,
212        original_column: u32,
213        name: u32,
214    ) {
215        self.mappings.push(Mapping {
216            generated_line,
217            generated_column,
218            source: Some(source),
219            original_line,
220            original_column,
221            name: Some(name),
222            is_range_mapping: false,
223        });
224    }
225
226    /// Add a range mapping from generated position to original position.
227    ///
228    /// A range mapping maps every position from its generated position up to
229    /// (but not including) the next mapping, applying a proportional delta
230    /// to the original position (ECMA-426 `rangeMappings` proposal).
231    pub fn add_range_mapping(
232        &mut self,
233        generated_line: u32,
234        generated_column: u32,
235        source: u32,
236        original_line: u32,
237        original_column: u32,
238    ) {
239        self.mappings.push(Mapping {
240            generated_line,
241            generated_column,
242            source: Some(source),
243            original_line,
244            original_column,
245            name: None,
246            is_range_mapping: true,
247        });
248    }
249
250    /// Add a named range mapping.
251    pub fn add_named_range_mapping(
252        &mut self,
253        generated_line: u32,
254        generated_column: u32,
255        source: u32,
256        original_line: u32,
257        original_column: u32,
258        name: u32,
259    ) {
260        self.mappings.push(Mapping {
261            generated_line,
262            generated_column,
263            source: Some(source),
264            original_line,
265            original_column,
266            name: Some(name),
267            is_range_mapping: true,
268        });
269    }
270
271    /// Add a mapping only if it differs from the previous mapping on the same line.
272    ///
273    /// This skips redundant mappings where the source position is identical
274    /// to the last mapping, which reduces output size without losing information.
275    /// Used by bundlers and minifiers to avoid bloating source maps.
276    pub fn maybe_add_mapping(
277        &mut self,
278        generated_line: u32,
279        generated_column: u32,
280        source: u32,
281        original_line: u32,
282        original_column: u32,
283    ) -> bool {
284        if let Some(last) = self.mappings.last()
285            && last.generated_line == generated_line
286            && last.source == Some(source)
287            && last.original_line == original_line
288            && last.original_column == original_column
289        {
290            return false;
291        }
292        self.add_mapping(
293            generated_line,
294            generated_column,
295            source,
296            original_line,
297            original_column,
298        );
299        true
300    }
301
302    /// Encode all mappings to a VLQ-encoded string.
303    fn encode_mappings(&self) -> String {
304        if self.mappings.is_empty() {
305            return String::new();
306        }
307
308        if self.assume_sorted {
309            return Self::encode_sequential_impl(&self.mappings);
310        }
311
312        // Sort mappings by (generated_line, generated_column)
313        let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
314        sorted.sort_unstable_by(|a, b| {
315            a.generated_line
316                .cmp(&b.generated_line)
317                .then(a.generated_column.cmp(&b.generated_column))
318        });
319
320        #[cfg(feature = "parallel")]
321        if sorted.len() >= 4096 {
322            return Self::encode_parallel_impl(&sorted);
323        }
324
325        Self::encode_sequential_impl(&sorted)
326    }
327
328    #[inline]
329    fn encode_sequential_impl<T: std::borrow::Borrow<Mapping>>(sorted: &[T]) -> String {
330        // Pre-allocate enough for all mappings: up to 5 VLQ values × 7 bytes + separators
331        let mut out: Vec<u8> = Vec::with_capacity(sorted.len() * 36);
332
333        let mut prev_gen_col: i64 = 0;
334        let mut prev_source: i64 = 0;
335        let mut prev_orig_line: i64 = 0;
336        let mut prev_orig_col: i64 = 0;
337        let mut prev_name: i64 = 0;
338        let mut prev_gen_line: u32 = 0;
339        let mut first_in_line = true;
340
341        for item in sorted {
342            let m = item.borrow();
343            while prev_gen_line < m.generated_line {
344                out.push(b';');
345                prev_gen_line += 1;
346                prev_gen_col = 0;
347                first_in_line = true;
348            }
349
350            if !first_in_line {
351                out.push(b',');
352            }
353            first_in_line = false;
354
355            // SAFETY: buffer pre-allocated with 36 bytes per mapping (5×7 + 1 separator).
356            // Each vlq_encode_unchecked writes at most 7 bytes.
357            unsafe {
358                vlq_encode_unchecked(&mut out, m.generated_column as i64 - prev_gen_col);
359                prev_gen_col = m.generated_column as i64;
360
361                if let Some(source) = m.source {
362                    vlq_encode_unchecked(&mut out, source as i64 - prev_source);
363                    prev_source = source as i64;
364
365                    vlq_encode_unchecked(&mut out, m.original_line as i64 - prev_orig_line);
366                    prev_orig_line = m.original_line as i64;
367
368                    vlq_encode_unchecked(&mut out, m.original_column as i64 - prev_orig_col);
369                    prev_orig_col = m.original_column as i64;
370
371                    if let Some(name) = m.name {
372                        vlq_encode_unchecked(&mut out, name as i64 - prev_name);
373                        prev_name = name as i64;
374                    }
375                }
376            }
377        }
378
379        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
380        // and we only add b';' and b',' — all valid UTF-8.
381        debug_assert!(out.is_ascii());
382        unsafe { String::from_utf8_unchecked(out) }
383    }
384
385    #[cfg(feature = "parallel")]
386    fn encode_parallel_impl(sorted: &[&Mapping]) -> String {
387        use rayon::prelude::*;
388
389        let max_line = sorted
390            .last()
391            .expect("encode_parallel_impl requires non-empty sorted slice")
392            .generated_line as usize;
393
394        // Build line ranges: (start_idx, end_idx) into sorted slice
395        let mut line_ranges: Vec<(usize, usize)> = vec![(0, 0); max_line + 1];
396        let mut i = 0;
397        while i < sorted.len() {
398            let line = sorted[i].generated_line as usize;
399            let start = i;
400            while i < sorted.len() && sorted[i].generated_line as usize == line {
401                i += 1;
402            }
403            line_ranges[line] = (start, i);
404        }
405
406        // Sequential scan: compute cumulative state at each line boundary
407        let mut states: Vec<(i64, i64, i64, i64)> = Vec::with_capacity(max_line + 1);
408        let mut prev_source: i64 = 0;
409        let mut prev_orig_line: i64 = 0;
410        let mut prev_orig_col: i64 = 0;
411        let mut prev_name: i64 = 0;
412
413        for &(start, end) in &line_ranges {
414            states.push((prev_source, prev_orig_line, prev_orig_col, prev_name));
415            for m in &sorted[start..end] {
416                if let Some(source) = m.source {
417                    prev_source = source as i64;
418                    prev_orig_line = m.original_line as i64;
419                    prev_orig_col = m.original_column as i64;
420                    if let Some(name) = m.name {
421                        prev_name = name as i64;
422                    }
423                }
424            }
425        }
426
427        // Parallel: encode each line independently
428        let encoded_lines: Vec<Vec<u8>> = line_ranges
429            .par_iter()
430            .zip(states.par_iter())
431            .map(|(&(start, end), &(s, ol, oc, n))| {
432                if start == end {
433                    return Vec::new();
434                }
435                encode_mapping_slice(&sorted[start..end], s, ol, oc, n)
436            })
437            .collect();
438
439        // Join with semicolons
440        let total_len = encoded_lines.iter().map(|l| l.len()).sum::<usize>() + max_line;
441        let mut out: Vec<u8> = Vec::with_capacity(total_len);
442        for (i, bytes) in encoded_lines.iter().enumerate() {
443            if i > 0 {
444                out.push(b';');
445            }
446            out.extend_from_slice(bytes);
447        }
448
449        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
450        // and we only add b';' — all valid UTF-8.
451        debug_assert!(out.is_ascii());
452        unsafe { String::from_utf8_unchecked(out) }
453    }
454
455    /// Encode range mappings to a VLQ string.
456    /// Returns `None` if no range mappings exist.
457    fn encode_range_mappings(&self) -> Option<String> {
458        if !self.mappings.iter().any(|m| m.is_range_mapping) {
459            return None;
460        }
461
462        let ordered: Vec<&Mapping> = if self.assume_sorted {
463            self.mappings.iter().collect()
464        } else {
465            let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
466            sorted.sort_unstable_by(|a, b| {
467                a.generated_line
468                    .cmp(&b.generated_line)
469                    .then(a.generated_column.cmp(&b.generated_column))
470            });
471            sorted
472        };
473
474        let max_line = ordered.last().map_or(0, |m| m.generated_line);
475        let mut out: Vec<u8> = Vec::new();
476        let mut ordered_idx = 0;
477
478        for line in 0..=max_line {
479            if line > 0 {
480                out.push(b';');
481            }
482            let mut prev_offset: u64 = 0;
483            let mut first_on_line = true;
484            let mut line_local_idx: u64 = 0;
485
486            while ordered_idx < ordered.len() && ordered[ordered_idx].generated_line == line {
487                if ordered[ordered_idx].is_range_mapping {
488                    if !first_on_line {
489                        out.push(b',');
490                    }
491                    first_on_line = false;
492                    let delta = line_local_idx - prev_offset;
493                    vlq_encode_unsigned(&mut out, delta);
494                    prev_offset = line_local_idx;
495                }
496                line_local_idx += 1;
497                ordered_idx += 1;
498            }
499        }
500
501        while out.last() == Some(&b';') {
502            out.pop();
503        }
504
505        if out.is_empty() {
506            return None;
507        }
508
509        // SAFETY: vlq_encode_unsigned only pushes ASCII base64 chars,
510        // and we only add b';' and b',' — all valid UTF-8.
511        debug_assert!(out.is_ascii());
512        Some(unsafe { String::from_utf8_unchecked(out) })
513    }
514
515    /// Generate the source map as a JSON string.
516    pub fn to_json(&self) -> String {
517        use std::io::Write;
518
519        let mappings = self.encode_mappings();
520
521        // Encode scopes (may introduce names not yet in self.names)
522        let (scopes_str, names_for_json) = if let Some(ref scopes_info) = self.scopes {
523            let mut names = self.names.clone();
524            let s = srcmap_scopes::encode_scopes(scopes_info, &mut names);
525            (Some(s), std::borrow::Cow::Owned(names))
526        } else {
527            (None, std::borrow::Cow::Borrowed(&self.names))
528        };
529
530        // Better capacity estimate to avoid reallocs
531        let sources_size: usize = self.sources.iter().map(|s| s.len() + 4).sum();
532        let names_size: usize = names_for_json.iter().map(|n| n.len() + 4).sum();
533        let content_size: usize = self
534            .sources_content
535            .iter()
536            .map(|c| c.as_ref().map_or(5, |s| s.len() + 4))
537            .sum();
538        let capacity = 100 + sources_size + names_size + mappings.len() + content_size;
539
540        let mut json: Vec<u8> = Vec::with_capacity(capacity);
541        json.extend_from_slice(br#"{"version":3"#);
542
543        if let Some(ref file) = self.file {
544            json.extend_from_slice(br#","file":"#);
545            json_quote_into(&mut json, file);
546        }
547
548        if let Some(ref root) = self.source_root {
549            json.extend_from_slice(br#","sourceRoot":"#);
550            json_quote_into(&mut json, root);
551        }
552
553        // sources
554        json.extend_from_slice(br#","sources":["#);
555        for (i, s) in self.sources.iter().enumerate() {
556            if i > 0 {
557                json.push(b',');
558            }
559            json_quote_into(&mut json, s);
560        }
561        json.push(b']');
562
563        // sourcesContent (only if any content is set)
564        if self.sources_content.iter().any(|c| c.is_some()) {
565            json.extend_from_slice(br#","sourcesContent":["#);
566
567            #[cfg(feature = "parallel")]
568            {
569                use rayon::prelude::*;
570
571                let total_content: usize = self
572                    .sources_content
573                    .iter()
574                    .map(|c| c.as_ref().map_or(0, |s| s.len()))
575                    .sum();
576
577                if self.sources_content.len() >= 8 && total_content >= 8192 {
578                    let quoted: Vec<String> = self
579                        .sources_content
580                        .par_iter()
581                        .map(|c| match c {
582                            Some(content) => json_quote(content),
583                            None => "null".to_string(),
584                        })
585                        .collect();
586                    for (i, q) in quoted.iter().enumerate() {
587                        if i > 0 {
588                            json.push(b',');
589                        }
590                        json.extend_from_slice(q.as_bytes());
591                    }
592                } else {
593                    for (i, c) in self.sources_content.iter().enumerate() {
594                        if i > 0 {
595                            json.push(b',');
596                        }
597                        match c {
598                            Some(content) => json_quote_into(&mut json, content),
599                            None => json.extend_from_slice(b"null"),
600                        }
601                    }
602                }
603            }
604
605            #[cfg(not(feature = "parallel"))]
606            for (i, c) in self.sources_content.iter().enumerate() {
607                if i > 0 {
608                    json.push(b',');
609                }
610                match c {
611                    Some(content) => json_quote_into(&mut json, content),
612                    None => json.extend_from_slice(b"null"),
613                }
614            }
615
616            json.push(b']');
617        }
618
619        // names
620        json.extend_from_slice(br#","names":["#);
621        for (i, n) in names_for_json.iter().enumerate() {
622            if i > 0 {
623                json.push(b',');
624            }
625            json_quote_into(&mut json, n);
626        }
627        json.push(b']');
628
629        // mappings — VLQ string is pure base64/,/; so no escaping needed
630        json.extend_from_slice(br#","mappings":""#);
631        json.extend_from_slice(mappings.as_bytes());
632        json.push(b'"');
633
634        // ignoreList
635        if !self.ignore_list.is_empty() {
636            json.extend_from_slice(br#","ignoreList":["#);
637            for (i, &idx) in self.ignore_list.iter().enumerate() {
638                if i > 0 {
639                    json.push(b',');
640                }
641                let _ = write!(json, "{idx}");
642            }
643            json.push(b']');
644        }
645
646        // rangeMappings (only if any range mappings exist)
647        if let Some(ref range_mappings) = self.encode_range_mappings() {
648            json.extend_from_slice(br#","rangeMappings":""#);
649            json.extend_from_slice(range_mappings.as_bytes());
650            json.push(b'"');
651        }
652
653        // debugId
654        if let Some(ref id) = self.debug_id {
655            json.extend_from_slice(br#","debugId":"#);
656            json_quote_into(&mut json, id);
657        }
658
659        // scopes (ECMA-426 scopes proposal)
660        if let Some(ref s) = scopes_str {
661            json.extend_from_slice(br#","scopes":"#);
662            json_quote_into(&mut json, s);
663        }
664
665        json.push(b'}');
666
667        // SAFETY: all content written is valid UTF-8 — ASCII JSON syntax, base64 VLQ,
668        // and original UTF-8 strings passed through json_quote_into.
669        unsafe { String::from_utf8_unchecked(json) }
670    }
671
672    /// Get the number of mappings.
673    pub fn mapping_count(&self) -> usize {
674        self.mappings.len()
675    }
676
677    /// Directly construct a `SourceMap` from the generator's internal state.
678    ///
679    /// This avoids the encode-then-decode round-trip (VLQ encode to JSON string,
680    /// then re-parse) that would otherwise be needed in composition pipelines.
681    pub fn to_decoded_map(&self) -> srcmap_sourcemap::SourceMap {
682        let sm_mappings: Vec<srcmap_sourcemap::Mapping> = if self.assume_sorted {
683            // Skip sort — iterate directly over the mappings vec
684            self.mappings
685                .iter()
686                .map(|m| srcmap_sourcemap::Mapping {
687                    generated_line: m.generated_line,
688                    generated_column: m.generated_column,
689                    source: m.source.unwrap_or(u32::MAX),
690                    original_line: m.original_line,
691                    original_column: m.original_column,
692                    name: m.name.unwrap_or(u32::MAX),
693                    is_range_mapping: m.is_range_mapping,
694                })
695                .collect()
696        } else {
697            // Sort mappings by (generated_line, generated_column) — same as encode_mappings
698            let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
699            sorted.sort_unstable_by(|a, b| {
700                a.generated_line
701                    .cmp(&b.generated_line)
702                    .then(a.generated_column.cmp(&b.generated_column))
703            });
704            sorted
705                .iter()
706                .map(|m| srcmap_sourcemap::Mapping {
707                    generated_line: m.generated_line,
708                    generated_column: m.generated_column,
709                    source: m.source.unwrap_or(u32::MAX),
710                    original_line: m.original_line,
711                    original_column: m.original_column,
712                    name: m.name.unwrap_or(u32::MAX),
713                    is_range_mapping: m.is_range_mapping,
714                })
715                .collect()
716        };
717
718        // Build sources_content: convert Vec<Option<String>> → Vec<Option<String>>
719        let sources_content: Vec<Option<String>> = self.sources_content.clone();
720
721        // Build the source root-prefixed sources (matching what from_json does)
722        let sources: Vec<String> = match &self.source_root {
723            Some(root) if !root.is_empty() => {
724                self.sources.iter().map(|s| format!("{root}{s}")).collect()
725            }
726            _ => self.sources.clone(),
727        };
728
729        srcmap_sourcemap::SourceMap::from_parts(
730            self.file.clone(),
731            self.source_root.clone(),
732            sources,
733            sources_content,
734            self.names.clone(),
735            sm_mappings,
736            self.ignore_list.clone(),
737            self.debug_id.clone(),
738            None, // scopes are not included in decoded map (would need encoding/decoding)
739        )
740    }
741}
742
743/// Source map generator that encodes VLQ on-the-fly.
744///
745/// Unlike [`SourceMapGenerator`], which collects all mappings and sorts them
746/// at finalization, `StreamingGenerator` encodes each mapping to VLQ immediately.
747/// Mappings **must** be added in sorted order `(generated_line, generated_column)`.
748///
749/// This avoids intermediate `Vec<Mapping>` allocation, making it ideal for
750/// streaming composition pipelines.
751///
752/// # Examples
753///
754/// ```rust
755/// use srcmap_generator::StreamingGenerator;
756///
757/// let mut sg = StreamingGenerator::new(Some("bundle.js".to_string()));
758/// let src = sg.add_source("src/app.ts");
759/// sg.set_source_content(src, "const x = 1;".to_string());
760///
761/// // Mappings must be added in order
762/// sg.add_mapping(0, 0, src, 0, 6);
763/// sg.add_mapping(1, 0, src, 1, 0);
764///
765/// let json = sg.to_json();
766/// assert!(json.contains(r#""version":3"#));
767/// ```
768#[derive(Debug)]
769pub struct StreamingGenerator {
770    file: Option<String>,
771    source_root: Option<String>,
772    sources: Vec<String>,
773    sources_content: Vec<Option<String>>,
774    names: Vec<String>,
775    ignore_list: Vec<u32>,
776    debug_id: Option<String>,
777
778    // Dedup maps
779    source_map: FxHashMap<String, u32>,
780    name_map: FxHashMap<String, u32>,
781
782    // Streaming VLQ state
783    vlq_out: Vec<u8>,
784    prev_gen_line: u32,
785    prev_gen_col: i64,
786    prev_source: i64,
787    prev_orig_line: i64,
788    prev_orig_col: i64,
789    prev_name: i64,
790    first_in_line: bool,
791    mapping_count: usize,
792
793    // Range mapping tracking
794    line_local_index: u32,
795    range_entries: Vec<(u32, u32)>,
796}
797
798impl StreamingGenerator {
799    /// Create a new streaming source map generator.
800    pub fn new(file: Option<String>) -> Self {
801        Self {
802            file,
803            source_root: None,
804            sources: Vec::new(),
805            sources_content: Vec::new(),
806            names: Vec::new(),
807            ignore_list: Vec::new(),
808            debug_id: None,
809            source_map: FxHashMap::default(),
810            name_map: FxHashMap::default(),
811            vlq_out: Vec::with_capacity(1024),
812            prev_gen_line: 0,
813            prev_gen_col: 0,
814            prev_source: 0,
815            prev_orig_line: 0,
816            prev_orig_col: 0,
817            prev_name: 0,
818            first_in_line: true,
819            mapping_count: 0,
820            line_local_index: 0,
821            range_entries: Vec::new(),
822        }
823    }
824
825    /// Set the source root prefix.
826    pub fn set_source_root(&mut self, root: impl Into<String>) {
827        self.source_root = Some(root.into());
828    }
829
830    /// Set the debug ID (UUID) for this source map (ECMA-426).
831    pub fn set_debug_id(&mut self, id: impl Into<String>) {
832        self.debug_id = Some(id.into());
833    }
834
835    /// Register a source file and return its index.
836    #[inline]
837    pub fn add_source(&mut self, source: &str) -> u32 {
838        if let Some(&idx) = self.source_map.get(source) {
839            return idx;
840        }
841        let idx = self.sources.len() as u32;
842        self.sources.push(source.to_string());
843        self.sources_content.push(None);
844        self.source_map.insert(source.to_string(), idx);
845        idx
846    }
847
848    /// Set the content for a source file.
849    pub fn set_source_content(&mut self, source_idx: u32, content: impl Into<String>) {
850        if (source_idx as usize) < self.sources_content.len() {
851            self.sources_content[source_idx as usize] = Some(content.into());
852        }
853    }
854
855    /// Register a name and return its index.
856    #[inline]
857    pub fn add_name(&mut self, name: &str) -> u32 {
858        if let Some(&idx) = self.name_map.get(name) {
859            return idx;
860        }
861        let idx = self.names.len() as u32;
862        self.names.push(name.to_string());
863        self.name_map.insert(name.to_string(), idx);
864        idx
865    }
866
867    /// Add a source index to the ignore list.
868    pub fn add_to_ignore_list(&mut self, source_idx: u32) {
869        if !self.ignore_list.contains(&source_idx) {
870            self.ignore_list.push(source_idx);
871        }
872    }
873
874    /// Add a mapping with no source information (generated-only).
875    ///
876    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
877    #[inline]
878    pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
879        self.advance_to_line(generated_line);
880
881        if !self.first_in_line {
882            self.vlq_out.push(b',');
883        }
884        self.first_in_line = false;
885
886        vlq_encode(
887            &mut self.vlq_out,
888            generated_column as i64 - self.prev_gen_col,
889        );
890        self.prev_gen_col = generated_column as i64;
891        self.line_local_index += 1;
892        self.mapping_count += 1;
893    }
894
895    /// Add a mapping from generated position to original position.
896    ///
897    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
898    #[inline]
899    pub fn add_mapping(
900        &mut self,
901        generated_line: u32,
902        generated_column: u32,
903        source: u32,
904        original_line: u32,
905        original_column: u32,
906    ) {
907        self.advance_to_line(generated_line);
908
909        if !self.first_in_line {
910            self.vlq_out.push(b',');
911        }
912        self.first_in_line = false;
913
914        vlq_encode(
915            &mut self.vlq_out,
916            generated_column as i64 - self.prev_gen_col,
917        );
918        self.prev_gen_col = generated_column as i64;
919
920        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
921        self.prev_source = source as i64;
922
923        vlq_encode(
924            &mut self.vlq_out,
925            original_line as i64 - self.prev_orig_line,
926        );
927        self.prev_orig_line = original_line as i64;
928
929        vlq_encode(
930            &mut self.vlq_out,
931            original_column as i64 - self.prev_orig_col,
932        );
933        self.prev_orig_col = original_column as i64;
934
935        self.line_local_index += 1;
936        self.mapping_count += 1;
937    }
938
939    /// Add a range mapping from generated position to original position.
940    ///
941    /// Same as [`add_mapping`](Self::add_mapping) but marks this mapping as a range mapping
942    /// (ECMA-426). Mappings must be added in sorted order `(generated_line, generated_column)`.
943    #[inline]
944    pub fn add_range_mapping(
945        &mut self,
946        generated_line: u32,
947        generated_column: u32,
948        source: u32,
949        original_line: u32,
950        original_column: u32,
951    ) {
952        self.advance_to_line(generated_line);
953        self.range_entries
954            .push((self.prev_gen_line, self.line_local_index));
955
956        if !self.first_in_line {
957            self.vlq_out.push(b',');
958        }
959        self.first_in_line = false;
960
961        vlq_encode(
962            &mut self.vlq_out,
963            generated_column as i64 - self.prev_gen_col,
964        );
965        self.prev_gen_col = generated_column as i64;
966
967        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
968        self.prev_source = source as i64;
969
970        vlq_encode(
971            &mut self.vlq_out,
972            original_line as i64 - self.prev_orig_line,
973        );
974        self.prev_orig_line = original_line as i64;
975
976        vlq_encode(
977            &mut self.vlq_out,
978            original_column as i64 - self.prev_orig_col,
979        );
980        self.prev_orig_col = original_column as i64;
981
982        self.line_local_index += 1;
983        self.mapping_count += 1;
984    }
985
986    /// Add a mapping with a name.
987    ///
988    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
989    #[inline]
990    pub fn add_named_mapping(
991        &mut self,
992        generated_line: u32,
993        generated_column: u32,
994        source: u32,
995        original_line: u32,
996        original_column: u32,
997        name: u32,
998    ) {
999        self.advance_to_line(generated_line);
1000
1001        if !self.first_in_line {
1002            self.vlq_out.push(b',');
1003        }
1004        self.first_in_line = false;
1005
1006        vlq_encode(
1007            &mut self.vlq_out,
1008            generated_column as i64 - self.prev_gen_col,
1009        );
1010        self.prev_gen_col = generated_column as i64;
1011
1012        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
1013        self.prev_source = source as i64;
1014
1015        vlq_encode(
1016            &mut self.vlq_out,
1017            original_line as i64 - self.prev_orig_line,
1018        );
1019        self.prev_orig_line = original_line as i64;
1020
1021        vlq_encode(
1022            &mut self.vlq_out,
1023            original_column as i64 - self.prev_orig_col,
1024        );
1025        self.prev_orig_col = original_column as i64;
1026
1027        vlq_encode(&mut self.vlq_out, name as i64 - self.prev_name);
1028        self.prev_name = name as i64;
1029
1030        self.line_local_index += 1;
1031        self.mapping_count += 1;
1032    }
1033
1034    /// Add a named range mapping from generated position to original position.
1035    ///
1036    /// Same as [`add_named_mapping`](Self::add_named_mapping) but marks this mapping as a range
1037    /// mapping (ECMA-426). Mappings must be added in sorted order
1038    /// `(generated_line, generated_column)`.
1039    #[inline]
1040    pub fn add_named_range_mapping(
1041        &mut self,
1042        generated_line: u32,
1043        generated_column: u32,
1044        source: u32,
1045        original_line: u32,
1046        original_column: u32,
1047        name: u32,
1048    ) {
1049        self.advance_to_line(generated_line);
1050        self.range_entries
1051            .push((self.prev_gen_line, self.line_local_index));
1052
1053        if !self.first_in_line {
1054            self.vlq_out.push(b',');
1055        }
1056        self.first_in_line = false;
1057
1058        vlq_encode(
1059            &mut self.vlq_out,
1060            generated_column as i64 - self.prev_gen_col,
1061        );
1062        self.prev_gen_col = generated_column as i64;
1063
1064        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
1065        self.prev_source = source as i64;
1066
1067        vlq_encode(
1068            &mut self.vlq_out,
1069            original_line as i64 - self.prev_orig_line,
1070        );
1071        self.prev_orig_line = original_line as i64;
1072
1073        vlq_encode(
1074            &mut self.vlq_out,
1075            original_column as i64 - self.prev_orig_col,
1076        );
1077        self.prev_orig_col = original_column as i64;
1078
1079        vlq_encode(&mut self.vlq_out, name as i64 - self.prev_name);
1080        self.prev_name = name as i64;
1081
1082        self.line_local_index += 1;
1083        self.mapping_count += 1;
1084    }
1085
1086    /// Get the number of mappings added so far.
1087    pub fn mapping_count(&self) -> usize {
1088        self.mapping_count
1089    }
1090
1091    /// Advance VLQ output to the given generated line, emitting semicolons.
1092    #[inline]
1093    fn advance_to_line(&mut self, generated_line: u32) {
1094        while self.prev_gen_line < generated_line {
1095            self.vlq_out.push(b';');
1096            self.prev_gen_line += 1;
1097            self.prev_gen_col = 0;
1098            self.first_in_line = true;
1099            self.line_local_index = 0;
1100        }
1101    }
1102
1103    /// Generate the source map as a JSON string.
1104    pub fn to_json(&self) -> String {
1105        use std::io::Write;
1106
1107        let vlq = self.vlq_string();
1108
1109        // Better capacity estimate to avoid reallocs
1110        let sources_size: usize = self.sources.iter().map(|s| s.len() + 4).sum();
1111        let names_size: usize = self.names.iter().map(|n| n.len() + 4).sum();
1112        let content_size: usize = self
1113            .sources_content
1114            .iter()
1115            .map(|c| c.as_ref().map_or(5, |s| s.len() + 4))
1116            .sum();
1117        let capacity = 100 + sources_size + names_size + vlq.len() + content_size;
1118
1119        let mut json: Vec<u8> = Vec::with_capacity(capacity);
1120        json.extend_from_slice(br#"{"version":3"#);
1121
1122        if let Some(ref file) = self.file {
1123            json.extend_from_slice(br#","file":"#);
1124            json_quote_into(&mut json, file);
1125        }
1126
1127        if let Some(ref root) = self.source_root {
1128            json.extend_from_slice(br#","sourceRoot":"#);
1129            json_quote_into(&mut json, root);
1130        }
1131
1132        json.extend_from_slice(br#","sources":["#);
1133        for (i, s) in self.sources.iter().enumerate() {
1134            if i > 0 {
1135                json.push(b',');
1136            }
1137            json_quote_into(&mut json, s);
1138        }
1139        json.push(b']');
1140
1141        if self.sources_content.iter().any(|c| c.is_some()) {
1142            json.extend_from_slice(br#","sourcesContent":["#);
1143            for (i, c) in self.sources_content.iter().enumerate() {
1144                if i > 0 {
1145                    json.push(b',');
1146                }
1147                match c {
1148                    Some(content) => json_quote_into(&mut json, content),
1149                    None => json.extend_from_slice(b"null"),
1150                }
1151            }
1152            json.push(b']');
1153        }
1154
1155        json.extend_from_slice(br#","names":["#);
1156        for (i, n) in self.names.iter().enumerate() {
1157            if i > 0 {
1158                json.push(b',');
1159            }
1160            json_quote_into(&mut json, n);
1161        }
1162        json.push(b']');
1163
1164        // VLQ string is pure base64/,/; — no escaping needed
1165        json.extend_from_slice(br#","mappings":""#);
1166        json.extend_from_slice(vlq.as_bytes());
1167        json.push(b'"');
1168
1169        if !self.ignore_list.is_empty() {
1170            json.extend_from_slice(br#","ignoreList":["#);
1171            for (i, &idx) in self.ignore_list.iter().enumerate() {
1172                if i > 0 {
1173                    json.push(b',');
1174                }
1175                let _ = write!(json, "{idx}");
1176            }
1177            json.push(b']');
1178        }
1179
1180        if let Some(ref range_mappings) = self.encode_range_mappings() {
1181            json.extend_from_slice(br#","rangeMappings":""#);
1182            json.extend_from_slice(range_mappings.as_bytes());
1183            json.push(b'"');
1184        }
1185
1186        if let Some(ref id) = self.debug_id {
1187            json.extend_from_slice(br#","debugId":"#);
1188            json_quote_into(&mut json, id);
1189        }
1190
1191        json.push(b'}');
1192
1193        // SAFETY: all content written is valid UTF-8 — ASCII JSON syntax, base64 VLQ,
1194        // and original UTF-8 strings passed through json_quote_into.
1195        unsafe { String::from_utf8_unchecked(json) }
1196    }
1197
1198    /// Directly construct a `SourceMap` from the streaming generator's state.
1199    ///
1200    /// Parses the already-encoded VLQ mappings to build a decoded `SourceMap`.
1201    /// More efficient than `to_json()` + `SourceMap::from_json()` since it
1202    /// skips JSON generation and parsing.
1203    ///
1204    /// # Panics
1205    ///
1206    /// Panics if the internal VLQ-encoded mappings string is corrupted or
1207    /// contains invalid VLQ sequences. This is not expected under normal use,
1208    /// since the streaming encoder always produces valid output.
1209    pub fn to_decoded_map(
1210        &self,
1211    ) -> Result<srcmap_sourcemap::SourceMap, srcmap_sourcemap::ParseError> {
1212        let vlq = self.vlq_string();
1213        let range_mappings = self.encode_range_mappings();
1214
1215        let sources: Vec<String> = match &self.source_root {
1216            Some(root) if !root.is_empty() => {
1217                self.sources.iter().map(|s| format!("{root}{s}")).collect()
1218            }
1219            _ => self.sources.clone(),
1220        };
1221
1222        srcmap_sourcemap::SourceMap::from_vlq_with_range_mappings(
1223            &vlq,
1224            sources,
1225            self.names.clone(),
1226            self.file.clone(),
1227            self.source_root.clone(),
1228            self.sources_content.clone(),
1229            self.ignore_list.clone(),
1230            self.debug_id.clone(),
1231            range_mappings.as_deref(),
1232        )
1233    }
1234
1235    /// Encode range mapping entries to a VLQ string.
1236    /// Returns `None` if no range mappings exist.
1237    fn encode_range_mappings(&self) -> Option<String> {
1238        if self.range_entries.is_empty() {
1239            return None;
1240        }
1241
1242        let max_line = self.range_entries.last().map_or(0, |&(line, _)| line);
1243        let mut out: Vec<u8> = Vec::new();
1244        let mut entry_idx = 0;
1245
1246        for line in 0..=max_line {
1247            if line > 0 {
1248                out.push(b';');
1249            }
1250            let mut prev_offset: u64 = 0;
1251            let mut first_on_line = true;
1252
1253            while entry_idx < self.range_entries.len() && self.range_entries[entry_idx].0 == line {
1254                if !first_on_line {
1255                    out.push(b',');
1256                }
1257                first_on_line = false;
1258                let local_idx = self.range_entries[entry_idx].1 as u64;
1259                let delta = local_idx - prev_offset;
1260                vlq_encode_unsigned(&mut out, delta);
1261                prev_offset = local_idx;
1262                entry_idx += 1;
1263            }
1264        }
1265
1266        while out.last() == Some(&b';') {
1267            out.pop();
1268        }
1269
1270        if out.is_empty() {
1271            return None;
1272        }
1273
1274        // SAFETY: VLQ output is always valid ASCII/UTF-8
1275        Some(unsafe { String::from_utf8_unchecked(out) })
1276    }
1277
1278    /// Get the VLQ mappings as a string, trimming trailing semicolons.
1279    fn vlq_string(&self) -> String {
1280        let end = self
1281            .vlq_out
1282            .iter()
1283            .rposition(|&b| b != b';')
1284            .map_or(0, |i| i + 1);
1285        // SAFETY: VLQ output is always valid ASCII/UTF-8
1286        unsafe { String::from_utf8_unchecked(self.vlq_out[..end].to_vec()) }
1287    }
1288}
1289
1290/// Encode a slice of mappings for a single line to VLQ bytes.
1291///
1292/// Generated column starts at 0 (reset per line).
1293/// Cumulative state is passed in from the sequential pre-scan.
1294#[cfg(feature = "parallel")]
1295fn encode_mapping_slice(
1296    mappings: &[&Mapping],
1297    init_source: i64,
1298    init_orig_line: i64,
1299    init_orig_col: i64,
1300    init_name: i64,
1301) -> Vec<u8> {
1302    let mut buf = Vec::with_capacity(mappings.len() * 6);
1303    let mut prev_gen_col: i64 = 0;
1304    let mut prev_source = init_source;
1305    let mut prev_orig_line = init_orig_line;
1306    let mut prev_orig_col = init_orig_col;
1307    let mut prev_name = init_name;
1308    let mut first = true;
1309
1310    for m in mappings {
1311        if !first {
1312            buf.push(b',');
1313        }
1314        first = false;
1315
1316        vlq_encode(&mut buf, m.generated_column as i64 - prev_gen_col);
1317        prev_gen_col = m.generated_column as i64;
1318
1319        if let Some(source) = m.source {
1320            vlq_encode(&mut buf, source as i64 - prev_source);
1321            prev_source = source as i64;
1322
1323            vlq_encode(&mut buf, m.original_line as i64 - prev_orig_line);
1324            prev_orig_line = m.original_line as i64;
1325
1326            vlq_encode(&mut buf, m.original_column as i64 - prev_orig_col);
1327            prev_orig_col = m.original_column as i64;
1328
1329            if let Some(name) = m.name {
1330                vlq_encode(&mut buf, name as i64 - prev_name);
1331                prev_name = name as i64;
1332            }
1333        }
1334    }
1335
1336    buf
1337}
1338
1339/// JSON-quote a string directly into a byte buffer (avoids UTF-8 validation overhead).
1340fn json_quote_into(out: &mut Vec<u8>, s: &str) {
1341    let bytes = s.as_bytes();
1342    out.push(b'"');
1343
1344    let mut start = 0;
1345    for (i, &b) in bytes.iter().enumerate() {
1346        let escape: &[u8] = match b {
1347            b'"' => b"\\\"",
1348            b'\\' => b"\\\\",
1349            b'\n' => b"\\n",
1350            b'\r' => b"\\r",
1351            b'\t' => b"\\t",
1352            0x00..=0x1f => {
1353                out.extend_from_slice(&bytes[start..i]);
1354                let hex = b"0123456789abcdef";
1355                out.extend_from_slice(&[
1356                    b'\\',
1357                    b'u',
1358                    b'0',
1359                    b'0',
1360                    hex[(b >> 4) as usize],
1361                    hex[(b & 0xf) as usize],
1362                ]);
1363                start = i + 1;
1364                continue;
1365            }
1366            _ => continue,
1367        };
1368        out.extend_from_slice(&bytes[start..i]);
1369        out.extend_from_slice(escape);
1370        start = i + 1;
1371    }
1372
1373    out.extend_from_slice(&bytes[start..]);
1374    out.push(b'"');
1375}
1376
1377/// JSON-quote a string, returning a new String (used in parallel contexts).
1378#[cfg(feature = "parallel")]
1379fn json_quote(s: &str) -> String {
1380    let mut out = Vec::with_capacity(s.len() + 2);
1381    json_quote_into(&mut out, s);
1382    // SAFETY: json_quote_into only writes valid UTF-8 (ASCII escapes + original UTF-8 content).
1383    unsafe { String::from_utf8_unchecked(out) }
1384}
1385
1386// ── Tests ──────────────────────────────────────────────────────────
1387
1388#[cfg(test)]
1389mod tests {
1390    use super::*;
1391
1392    #[test]
1393    fn empty_generator() {
1394        let builder = SourceMapGenerator::new(None);
1395        let json = builder.to_json();
1396        assert!(json.contains(r#""version":3"#));
1397        assert!(json.contains(r#""mappings":"""#));
1398    }
1399
1400    #[test]
1401    fn simple_mapping() {
1402        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
1403        let src = builder.add_source("input.js");
1404        builder.add_mapping(0, 0, src, 0, 0);
1405
1406        let json = builder.to_json();
1407        assert!(json.contains(r#""file":"output.js""#));
1408        assert!(json.contains(r#""sources":["input.js"]"#));
1409
1410        // Verify roundtrip with parser
1411        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1412        let loc = sm.original_position_for(0, 0).unwrap();
1413        assert_eq!(sm.source(loc.source), "input.js");
1414        assert_eq!(loc.line, 0);
1415        assert_eq!(loc.column, 0);
1416    }
1417
1418    #[test]
1419    fn mapping_with_name() {
1420        let mut builder = SourceMapGenerator::new(None);
1421        let src = builder.add_source("input.js");
1422        let name = builder.add_name("myFunction");
1423        builder.add_named_mapping(0, 0, src, 0, 0, name);
1424
1425        let json = builder.to_json();
1426        assert!(json.contains(r#""names":["myFunction"]"#));
1427
1428        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1429        let loc = sm.original_position_for(0, 0).unwrap();
1430        assert_eq!(loc.name, Some(0));
1431        assert_eq!(sm.name(0), "myFunction");
1432    }
1433
1434    #[test]
1435    fn multiple_lines() {
1436        let mut builder = SourceMapGenerator::new(None);
1437        let src = builder.add_source("input.js");
1438        builder.add_mapping(0, 0, src, 0, 0);
1439        builder.add_mapping(1, 4, src, 1, 2);
1440        builder.add_mapping(2, 0, src, 2, 0);
1441
1442        let json = builder.to_json();
1443        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1444        assert_eq!(sm.line_count(), 3);
1445
1446        let loc = sm.original_position_for(1, 4).unwrap();
1447        assert_eq!(loc.line, 1);
1448        assert_eq!(loc.column, 2);
1449    }
1450
1451    #[test]
1452    fn multiple_sources() {
1453        let mut builder = SourceMapGenerator::new(None);
1454        let a = builder.add_source("a.js");
1455        let b = builder.add_source("b.js");
1456        builder.add_mapping(0, 0, a, 0, 0);
1457        builder.add_mapping(1, 0, b, 0, 0);
1458
1459        let json = builder.to_json();
1460        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1461
1462        let loc0 = sm.original_position_for(0, 0).unwrap();
1463        let loc1 = sm.original_position_for(1, 0).unwrap();
1464        assert_eq!(sm.source(loc0.source), "a.js");
1465        assert_eq!(sm.source(loc1.source), "b.js");
1466    }
1467
1468    #[test]
1469    fn source_content() {
1470        let mut builder = SourceMapGenerator::new(None);
1471        let src = builder.add_source("input.js");
1472        builder.set_source_content(src, "var x = 1;".to_string());
1473        builder.add_mapping(0, 0, src, 0, 0);
1474
1475        let json = builder.to_json();
1476        assert!(json.contains(r#""sourcesContent":["var x = 1;"]"#));
1477
1478        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1479        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
1480    }
1481
1482    #[test]
1483    fn source_root() {
1484        let mut builder = SourceMapGenerator::new(None);
1485        builder.set_source_root("src/".to_string());
1486        let src = builder.add_source("input.js");
1487        builder.add_mapping(0, 0, src, 0, 0);
1488
1489        let json = builder.to_json();
1490        assert!(json.contains(r#""sourceRoot":"src/""#));
1491    }
1492
1493    #[test]
1494    fn ignore_list() {
1495        let mut builder = SourceMapGenerator::new(None);
1496        let _app = builder.add_source("app.js");
1497        let lib = builder.add_source("node_modules/lib.js");
1498        builder.add_to_ignore_list(lib);
1499        builder.add_mapping(0, 0, lib, 0, 0);
1500
1501        let json = builder.to_json();
1502        assert!(json.contains(r#""ignoreList":[1]"#));
1503    }
1504
1505    #[test]
1506    fn generated_only_mapping() {
1507        let mut builder = SourceMapGenerator::new(None);
1508        builder.add_generated_mapping(0, 0);
1509
1510        let json = builder.to_json();
1511        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1512        // Generated-only mapping → no source info
1513        assert!(sm.original_position_for(0, 0).is_none());
1514    }
1515
1516    #[test]
1517    fn dedup_sources_and_names() {
1518        let mut builder = SourceMapGenerator::new(None);
1519        let s1 = builder.add_source("input.js");
1520        let s2 = builder.add_source("input.js"); // duplicate
1521        assert_eq!(s1, s2);
1522
1523        let n1 = builder.add_name("foo");
1524        let n2 = builder.add_name("foo"); // duplicate
1525        assert_eq!(n1, n2);
1526
1527        assert_eq!(builder.sources.len(), 1);
1528        assert_eq!(builder.names.len(), 1);
1529    }
1530
1531    #[test]
1532    fn large_roundtrip() {
1533        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1534
1535        for i in 0..5 {
1536            builder.add_source(&format!("src/file{i}.js"));
1537        }
1538        for i in 0..10 {
1539            builder.add_name(&format!("var{i}"));
1540        }
1541
1542        // Add 1000 mappings across 100 lines
1543        for line in 0..100u32 {
1544            for col in 0..10u32 {
1545                let src = (line * 10 + col) % 5;
1546                let name = if col % 3 == 0 { Some(col % 10) } else { None };
1547
1548                match name {
1549                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
1550                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
1551                }
1552            }
1553        }
1554
1555        let json = builder.to_json();
1556        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1557
1558        assert_eq!(sm.mapping_count(), 1000);
1559        assert_eq!(sm.line_count(), 100);
1560
1561        // Verify a few lookups
1562        let loc = sm.original_position_for(50, 30).unwrap();
1563        assert_eq!(loc.line, 50);
1564        assert_eq!(loc.column, 15);
1565    }
1566
1567    #[test]
1568    fn json_escaping() {
1569        let mut builder = SourceMapGenerator::new(None);
1570        let src = builder.add_source("path/with\"quotes.js");
1571        builder.set_source_content(src, "line1\nline2\ttab".to_string());
1572        builder.add_mapping(0, 0, src, 0, 0);
1573
1574        let json = builder.to_json();
1575        // Should be valid JSON
1576        let _: serde_json::Value = serde_json::from_str(&json).unwrap();
1577    }
1578
1579    #[test]
1580    fn maybe_add_mapping_skips_redundant() {
1581        let mut builder = SourceMapGenerator::new(None);
1582        let src = builder.add_source("input.js");
1583
1584        // First mapping — always added
1585        assert!(builder.maybe_add_mapping(0, 0, src, 10, 0));
1586        // Same source position, different generated column — redundant, skipped
1587        assert!(!builder.maybe_add_mapping(0, 5, src, 10, 0));
1588        // Different source position — added
1589        assert!(builder.maybe_add_mapping(0, 10, src, 11, 0));
1590        // Different generated line, same source position as last — added (new line resets)
1591        assert!(builder.maybe_add_mapping(1, 0, src, 11, 0));
1592
1593        assert_eq!(builder.mapping_count(), 3);
1594
1595        let json = builder.to_json();
1596        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1597        assert_eq!(sm.mapping_count(), 3);
1598    }
1599
1600    #[test]
1601    fn maybe_add_mapping_different_source() {
1602        let mut builder = SourceMapGenerator::new(None);
1603        let a = builder.add_source("a.js");
1604        let b = builder.add_source("b.js");
1605
1606        assert!(builder.maybe_add_mapping(0, 0, a, 0, 0));
1607        // Same line/col but different source — not redundant
1608        assert!(builder.maybe_add_mapping(0, 5, b, 0, 0));
1609
1610        assert_eq!(builder.mapping_count(), 2);
1611    }
1612
1613    #[test]
1614    fn to_decoded_map_basic() {
1615        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
1616        let src = builder.add_source("input.js");
1617        builder.add_mapping(0, 0, src, 0, 0);
1618        builder.add_mapping(1, 4, src, 1, 2);
1619
1620        let sm = builder.to_decoded_map();
1621        assert_eq!(sm.mapping_count(), 2);
1622        assert_eq!(sm.line_count(), 2);
1623
1624        let loc = sm.original_position_for(0, 0).unwrap();
1625        assert_eq!(sm.source(loc.source), "input.js");
1626        assert_eq!(loc.line, 0);
1627        assert_eq!(loc.column, 0);
1628
1629        let loc = sm.original_position_for(1, 4).unwrap();
1630        assert_eq!(loc.line, 1);
1631        assert_eq!(loc.column, 2);
1632    }
1633
1634    #[test]
1635    fn to_decoded_map_with_names() {
1636        let mut builder = SourceMapGenerator::new(None);
1637        let src = builder.add_source("input.js");
1638        let name = builder.add_name("myFunction");
1639        builder.add_named_mapping(0, 0, src, 0, 0, name);
1640
1641        let sm = builder.to_decoded_map();
1642        let loc = sm.original_position_for(0, 0).unwrap();
1643        assert_eq!(loc.name, Some(0));
1644        assert_eq!(sm.name(0), "myFunction");
1645    }
1646
1647    #[test]
1648    fn to_decoded_map_matches_json_roundtrip() {
1649        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1650        for i in 0..5 {
1651            builder.add_source(&format!("src/file{i}.js"));
1652        }
1653        for i in 0..10 {
1654            builder.add_name(&format!("var{i}"));
1655        }
1656
1657        for line in 0..50u32 {
1658            for col in 0..10u32 {
1659                let src = (line * 10 + col) % 5;
1660                let name = if col % 3 == 0 { Some(col % 10) } else { None };
1661                match name {
1662                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
1663                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
1664                }
1665            }
1666        }
1667
1668        // Compare decoded map vs JSON roundtrip
1669        let sm_decoded = builder.to_decoded_map();
1670        let json = builder.to_json();
1671        let sm_json = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1672
1673        assert_eq!(sm_decoded.mapping_count(), sm_json.mapping_count());
1674        assert_eq!(sm_decoded.line_count(), sm_json.line_count());
1675
1676        // Verify all lookups match
1677        for m in sm_json.all_mappings() {
1678            let a = sm_json.original_position_for(m.generated_line, m.generated_column);
1679            let b = sm_decoded.original_position_for(m.generated_line, m.generated_column);
1680            match (a, b) {
1681                (Some(a), Some(b)) => {
1682                    assert_eq!(
1683                        a.source, b.source,
1684                        "source mismatch at ({}, {})",
1685                        m.generated_line, m.generated_column
1686                    );
1687                    assert_eq!(
1688                        a.line, b.line,
1689                        "line mismatch at ({}, {})",
1690                        m.generated_line, m.generated_column
1691                    );
1692                    assert_eq!(
1693                        a.column, b.column,
1694                        "column mismatch at ({}, {})",
1695                        m.generated_line, m.generated_column
1696                    );
1697                    assert_eq!(
1698                        a.name, b.name,
1699                        "name mismatch at ({}, {})",
1700                        m.generated_line, m.generated_column
1701                    );
1702                }
1703                (None, None) => {}
1704                _ => panic!(
1705                    "lookup mismatch at ({}, {})",
1706                    m.generated_line, m.generated_column
1707                ),
1708            }
1709        }
1710    }
1711
1712    #[test]
1713    fn to_decoded_map_empty() {
1714        let builder = SourceMapGenerator::new(None);
1715        let sm = builder.to_decoded_map();
1716        assert_eq!(sm.mapping_count(), 0);
1717        assert_eq!(sm.line_count(), 0);
1718    }
1719
1720    #[test]
1721    fn to_decoded_map_generated_only() {
1722        let mut builder = SourceMapGenerator::new(None);
1723        builder.add_generated_mapping(0, 0);
1724
1725        let sm = builder.to_decoded_map();
1726        assert_eq!(sm.mapping_count(), 1);
1727        // Generated-only mapping has no source info
1728        assert!(sm.original_position_for(0, 0).is_none());
1729    }
1730
1731    #[test]
1732    fn to_decoded_map_multiple_sources() {
1733        let mut builder = SourceMapGenerator::new(None);
1734        let a = builder.add_source("a.js");
1735        let b = builder.add_source("b.js");
1736        builder.add_mapping(0, 0, a, 0, 0);
1737        builder.add_mapping(1, 0, b, 0, 0);
1738
1739        let sm = builder.to_decoded_map();
1740        let loc0 = sm.original_position_for(0, 0).unwrap();
1741        let loc1 = sm.original_position_for(1, 0).unwrap();
1742        assert_eq!(sm.source(loc0.source), "a.js");
1743        assert_eq!(sm.source(loc1.source), "b.js");
1744    }
1745
1746    #[test]
1747    fn to_decoded_map_with_source_content() {
1748        let mut builder = SourceMapGenerator::new(None);
1749        let src = builder.add_source("input.js");
1750        builder.set_source_content(src, "var x = 1;".to_string());
1751        builder.add_mapping(0, 0, src, 0, 0);
1752
1753        let sm = builder.to_decoded_map();
1754        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
1755    }
1756
1757    #[test]
1758    fn to_decoded_map_reverse_lookup() {
1759        let mut builder = SourceMapGenerator::new(None);
1760        let src = builder.add_source("input.js");
1761        builder.add_mapping(0, 0, src, 10, 5);
1762
1763        let sm = builder.to_decoded_map();
1764        let loc = sm.generated_position_for("input.js", 10, 5).unwrap();
1765        assert_eq!(loc.line, 0);
1766        assert_eq!(loc.column, 0);
1767    }
1768
1769    #[test]
1770    fn to_decoded_map_sparse_lines() {
1771        let mut builder = SourceMapGenerator::new(None);
1772        let src = builder.add_source("input.js");
1773        builder.add_mapping(0, 0, src, 0, 0);
1774        builder.add_mapping(5, 0, src, 5, 0);
1775
1776        let sm = builder.to_decoded_map();
1777        assert_eq!(sm.line_count(), 6);
1778        assert!(sm.original_position_for(0, 0).is_some());
1779        assert!(sm.original_position_for(2, 0).is_none());
1780        assert!(sm.original_position_for(5, 0).is_some());
1781    }
1782
1783    #[test]
1784    fn empty_lines_between_mappings() {
1785        let mut builder = SourceMapGenerator::new(None);
1786        let src = builder.add_source("input.js");
1787        builder.add_mapping(0, 0, src, 0, 0);
1788        // Skip lines 1-4
1789        builder.add_mapping(5, 0, src, 5, 0);
1790
1791        let json = builder.to_json();
1792        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1793
1794        // Line 0 should have a mapping
1795        assert!(sm.original_position_for(0, 0).is_some());
1796        // Lines 1-4 should have no mappings
1797        assert!(sm.original_position_for(2, 0).is_none());
1798        // Line 5 should have a mapping
1799        assert!(sm.original_position_for(5, 0).is_some());
1800    }
1801
1802    #[test]
1803    fn debug_id() {
1804        let mut builder = SourceMapGenerator::new(None);
1805        builder.set_debug_id("85314830-023f-4cf1-a267-535f4e37bb17".to_string());
1806        let src = builder.add_source("input.js");
1807        builder.add_mapping(0, 0, src, 0, 0);
1808
1809        let json = builder.to_json();
1810        assert!(json.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
1811
1812        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1813        assert_eq!(
1814            sm.debug_id.as_deref(),
1815            Some("85314830-023f-4cf1-a267-535f4e37bb17")
1816        );
1817    }
1818
1819    #[test]
1820    fn scopes_roundtrip() {
1821        use srcmap_scopes::{Binding, GeneratedRange, OriginalScope, Position, ScopeInfo};
1822
1823        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1824        let src = builder.add_source("input.js");
1825        builder.set_source_content(
1826            src,
1827            "function hello(name) {\n  return name;\n}\nhello('world');".to_string(),
1828        );
1829        let name_hello = builder.add_name("hello");
1830        builder.add_named_mapping(0, 0, src, 0, 0, name_hello);
1831        builder.add_mapping(1, 0, src, 1, 0);
1832
1833        // Set scopes
1834        builder.set_scopes(ScopeInfo {
1835            scopes: vec![Some(OriginalScope {
1836                start: Position { line: 0, column: 0 },
1837                end: Position {
1838                    line: 3,
1839                    column: 14,
1840                },
1841                name: None,
1842                kind: Some("global".to_string()),
1843                is_stack_frame: false,
1844                variables: vec!["hello".to_string()],
1845                children: vec![OriginalScope {
1846                    start: Position { line: 0, column: 9 },
1847                    end: Position { line: 2, column: 1 },
1848                    name: Some("hello".to_string()),
1849                    kind: Some("function".to_string()),
1850                    is_stack_frame: true,
1851                    variables: vec!["name".to_string()],
1852                    children: vec![],
1853                }],
1854            })],
1855            ranges: vec![GeneratedRange {
1856                start: Position { line: 0, column: 0 },
1857                end: Position {
1858                    line: 3,
1859                    column: 14,
1860                },
1861                is_stack_frame: false,
1862                is_hidden: false,
1863                definition: Some(0),
1864                call_site: None,
1865                bindings: vec![Binding::Expression("hello".to_string())],
1866                children: vec![GeneratedRange {
1867                    start: Position { line: 0, column: 9 },
1868                    end: Position { line: 2, column: 1 },
1869                    is_stack_frame: true,
1870                    is_hidden: false,
1871                    definition: Some(1),
1872                    call_site: None,
1873                    bindings: vec![Binding::Expression("name".to_string())],
1874                    children: vec![],
1875                }],
1876            }],
1877        });
1878
1879        let json = builder.to_json();
1880
1881        // Verify scopes field is present
1882        assert!(json.contains(r#""scopes":"#));
1883
1884        // Parse back and verify
1885        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1886        assert!(sm.scopes.is_some());
1887
1888        let scopes_info = sm.scopes.unwrap();
1889
1890        // Verify original scopes
1891        assert_eq!(scopes_info.scopes.len(), 1);
1892        let root_scope = scopes_info.scopes[0].as_ref().unwrap();
1893        assert_eq!(root_scope.kind.as_deref(), Some("global"));
1894        assert_eq!(root_scope.variables, vec!["hello"]);
1895        assert_eq!(root_scope.children.len(), 1);
1896
1897        let fn_scope = &root_scope.children[0];
1898        assert_eq!(fn_scope.name.as_deref(), Some("hello"));
1899        assert_eq!(fn_scope.kind.as_deref(), Some("function"));
1900        assert!(fn_scope.is_stack_frame);
1901        assert_eq!(fn_scope.variables, vec!["name"]);
1902
1903        // Verify generated ranges
1904        assert_eq!(scopes_info.ranges.len(), 1);
1905        let outer = &scopes_info.ranges[0];
1906        assert_eq!(outer.definition, Some(0));
1907        assert_eq!(
1908            outer.bindings,
1909            vec![Binding::Expression("hello".to_string())]
1910        );
1911        assert_eq!(outer.children.len(), 1);
1912
1913        let inner = &outer.children[0];
1914        assert_eq!(inner.definition, Some(1));
1915        assert!(inner.is_stack_frame);
1916        assert_eq!(
1917            inner.bindings,
1918            vec![Binding::Expression("name".to_string())]
1919        );
1920    }
1921
1922    #[test]
1923    fn scopes_with_inlining_roundtrip() {
1924        use srcmap_scopes::{
1925            Binding, CallSite, GeneratedRange, OriginalScope, Position, ScopeInfo,
1926        };
1927
1928        let mut builder = SourceMapGenerator::new(None);
1929        let src = builder.add_source("input.js");
1930        builder.add_mapping(0, 0, src, 0, 0);
1931
1932        builder.set_scopes(ScopeInfo {
1933            scopes: vec![Some(OriginalScope {
1934                start: Position { line: 0, column: 0 },
1935                end: Position {
1936                    line: 10,
1937                    column: 0,
1938                },
1939                name: None,
1940                kind: None,
1941                is_stack_frame: false,
1942                variables: vec!["x".to_string()],
1943                children: vec![OriginalScope {
1944                    start: Position { line: 1, column: 0 },
1945                    end: Position { line: 4, column: 1 },
1946                    name: Some("greet".to_string()),
1947                    kind: Some("function".to_string()),
1948                    is_stack_frame: true,
1949                    variables: vec!["msg".to_string()],
1950                    children: vec![],
1951                }],
1952            })],
1953            ranges: vec![GeneratedRange {
1954                start: Position { line: 0, column: 0 },
1955                end: Position {
1956                    line: 10,
1957                    column: 0,
1958                },
1959                is_stack_frame: false,
1960                is_hidden: false,
1961                definition: Some(0),
1962                call_site: None,
1963                bindings: vec![Binding::Expression("_x".to_string())],
1964                children: vec![GeneratedRange {
1965                    start: Position { line: 6, column: 0 },
1966                    end: Position { line: 8, column: 0 },
1967                    is_stack_frame: true,
1968                    is_hidden: false,
1969                    definition: Some(1),
1970                    call_site: Some(CallSite {
1971                        source_index: 0,
1972                        line: 8,
1973                        column: 0,
1974                    }),
1975                    bindings: vec![Binding::Expression("\"Hello\"".to_string())],
1976                    children: vec![],
1977                }],
1978            }],
1979        });
1980
1981        let json = builder.to_json();
1982        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1983        let info = sm.scopes.unwrap();
1984
1985        // Verify call site on inlined range
1986        let inlined = &info.ranges[0].children[0];
1987        assert_eq!(
1988            inlined.call_site,
1989            Some(CallSite {
1990                source_index: 0,
1991                line: 8,
1992                column: 0,
1993            })
1994        );
1995        assert_eq!(
1996            inlined.bindings,
1997            vec![Binding::Expression("\"Hello\"".to_string())]
1998        );
1999    }
2000
2001    #[test]
2002    fn set_source_content_out_of_bounds() {
2003        let mut builder = SourceMapGenerator::new(None);
2004        // No sources added, index 0 is out of bounds
2005        builder.set_source_content(0, "content".to_string());
2006        // Should silently do nothing
2007        let json = builder.to_json();
2008        assert!(!json.contains("content"));
2009    }
2010
2011    #[test]
2012    fn add_to_ignore_list_dedup() {
2013        let mut builder = SourceMapGenerator::new(None);
2014        let idx = builder.add_source("vendor.js");
2015        builder.add_to_ignore_list(idx);
2016        builder.add_to_ignore_list(idx); // duplicate - should be deduped
2017        let json = builder.to_json();
2018        // Should only appear once in ignoreList
2019        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2020        assert_eq!(sm.ignore_list, vec![0]);
2021    }
2022
2023    #[test]
2024    fn to_decoded_map_with_source_root() {
2025        let mut builder = SourceMapGenerator::new(None);
2026        builder.set_source_root("src/".to_string());
2027        let src = builder.add_source("app.ts");
2028        builder.add_mapping(0, 0, src, 0, 0);
2029        let sm = builder.to_decoded_map();
2030        // Sources should be prefixed with source_root
2031        assert_eq!(sm.sources, vec!["src/app.ts"]);
2032    }
2033
2034    #[test]
2035    fn json_escaping_special_chars() {
2036        let mut builder = SourceMapGenerator::new(None);
2037        let src = builder.add_source("a.js");
2038        // Content with special chars: quotes, backslash, newline, carriage return, tab, control char
2039        builder.set_source_content(src, "line1\nline2\r\ttab\\\"\x01end".to_string());
2040        builder.add_mapping(0, 0, src, 0, 0);
2041        let json = builder.to_json();
2042        // Verify it's valid JSON by parsing
2043        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2044        assert_eq!(
2045            sm.sources_content,
2046            vec![Some("line1\nline2\r\ttab\\\"\x01end".to_string())]
2047        );
2048    }
2049
2050    #[test]
2051    fn json_escaping_in_names() {
2052        let mut builder = SourceMapGenerator::new(None);
2053        let src = builder.add_source("a.js");
2054        let name = builder.add_name("func\"with\\special");
2055        builder.add_named_mapping(0, 0, src, 0, 0, name);
2056        let json = builder.to_json();
2057        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2058        assert_eq!(sm.names[0], "func\"with\\special");
2059    }
2060
2061    #[test]
2062    fn json_escaping_in_sources() {
2063        let mut builder = SourceMapGenerator::new(None);
2064        let src = builder.add_source("path/with\"quotes.js");
2065        builder.add_mapping(0, 0, src, 0, 0);
2066        let json = builder.to_json();
2067        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2068        assert_eq!(sm.sources[0], "path/with\"quotes.js");
2069    }
2070
2071    #[cfg(feature = "parallel")]
2072    mod parallel_tests {
2073        use super::*;
2074
2075        fn build_large_generator(lines: u32, cols_per_line: u32) -> SourceMapGenerator {
2076            let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
2077            for i in 0..10 {
2078                let src = builder.add_source(&format!("src/file{i}.js"));
2079                builder.set_source_content(
2080                    src,
2081                    format!("// source file {i}\n{}", "x = 1;\n".repeat(100)),
2082                );
2083            }
2084            for i in 0..20 {
2085                builder.add_name(&format!("var{i}"));
2086            }
2087
2088            for line in 0..lines {
2089                for col in 0..cols_per_line {
2090                    let src = (line * cols_per_line + col) % 10;
2091                    let name = if col % 3 == 0 { Some(col % 20) } else { None };
2092                    match name {
2093                        Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
2094                        None => builder.add_mapping(line, col * 10, src, line, col * 5),
2095                    }
2096                }
2097            }
2098            builder
2099        }
2100
2101        #[test]
2102        fn parallel_large_roundtrip() {
2103            let builder = build_large_generator(500, 20);
2104            let json = builder.to_json();
2105            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2106            assert_eq!(sm.mapping_count(), 10000);
2107            assert_eq!(sm.line_count(), 500);
2108
2109            // Verify lookups
2110            let loc = sm.original_position_for(250, 50).unwrap();
2111            assert_eq!(loc.line, 250);
2112            assert_eq!(loc.column, 25);
2113        }
2114
2115        #[test]
2116        fn parallel_matches_sequential() {
2117            let builder = build_large_generator(500, 20);
2118
2119            // Sort mappings the same way encode_mappings does
2120            let mut sorted: Vec<&Mapping> = builder.mappings.iter().collect();
2121            sorted.sort_unstable_by(|a, b| {
2122                a.generated_line
2123                    .cmp(&b.generated_line)
2124                    .then(a.generated_column.cmp(&b.generated_column))
2125            });
2126
2127            let sequential = SourceMapGenerator::encode_sequential_impl(&sorted);
2128            let parallel = SourceMapGenerator::encode_parallel_impl(&sorted);
2129            assert_eq!(sequential, parallel);
2130        }
2131
2132        #[test]
2133        fn parallel_with_sparse_lines() {
2134            let mut builder = SourceMapGenerator::new(None);
2135            let src = builder.add_source("input.js");
2136
2137            // Add mappings on lines 0, 100, 200, ... (sparse)
2138            for i in 0..50 {
2139                let line = i * 100;
2140                for col in 0..100u32 {
2141                    builder.add_mapping(line, col * 10, src, line, col * 5);
2142                }
2143            }
2144
2145            let json = builder.to_json();
2146            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2147            assert_eq!(sm.mapping_count(), 5000);
2148
2149            // Verify empty lines have no mappings
2150            assert!(sm.original_position_for(50, 0).is_none());
2151            // Verify populated lines work
2152            let loc = sm.original_position_for(200, 50).unwrap();
2153            assert_eq!(loc.line, 200);
2154            assert_eq!(loc.column, 25);
2155        }
2156    }
2157
2158    // ── StreamingGenerator tests ────────────────────────────────
2159
2160    #[test]
2161    fn streaming_basic() {
2162        let mut sg = StreamingGenerator::new(Some("out.js".to_string()));
2163        let src = sg.add_source("input.js");
2164        sg.add_mapping(0, 0, src, 0, 0);
2165        sg.add_mapping(1, 0, src, 1, 0);
2166
2167        let json = sg.to_json();
2168        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2169        assert_eq!(sm.sources, vec!["input.js"]);
2170        assert_eq!(sm.mapping_count(), 2);
2171
2172        let loc0 = sm.original_position_for(0, 0).unwrap();
2173        assert_eq!(sm.source(loc0.source), "input.js");
2174        assert_eq!(loc0.line, 0);
2175
2176        let loc1 = sm.original_position_for(1, 0).unwrap();
2177        assert_eq!(loc1.line, 1);
2178    }
2179
2180    #[test]
2181    fn streaming_with_names() {
2182        let mut sg = StreamingGenerator::new(None);
2183        let src = sg.add_source("a.js");
2184        let name = sg.add_name("foo");
2185        sg.add_named_mapping(0, 0, src, 0, 0, name);
2186
2187        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2188        let loc = sm.original_position_for(0, 0).unwrap();
2189        assert_eq!(loc.name, Some(0));
2190        assert_eq!(sm.name(0), "foo");
2191    }
2192
2193    #[test]
2194    fn streaming_generated_only() {
2195        let mut sg = StreamingGenerator::new(None);
2196        let src = sg.add_source("a.js");
2197        sg.add_generated_mapping(0, 0);
2198        sg.add_mapping(0, 5, src, 0, 0);
2199
2200        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2201        assert_eq!(sm.mapping_count(), 2);
2202        assert!(sm.original_position_for(0, 0).is_none());
2203        assert!(sm.original_position_for(0, 5).is_some());
2204    }
2205
2206    #[test]
2207    fn streaming_matches_regular_generator() {
2208        let mut regular = SourceMapGenerator::new(Some("out.js".to_string()));
2209        let mut streaming = StreamingGenerator::new(Some("out.js".to_string()));
2210
2211        let src_r = regular.add_source("a.js");
2212        let src_s = streaming.add_source("a.js");
2213
2214        let name_r = regular.add_name("hello");
2215        let name_s = streaming.add_name("hello");
2216
2217        regular.set_source_content(src_r, "var hello;".to_string());
2218        streaming.set_source_content(src_s, "var hello;".to_string());
2219
2220        regular.add_named_mapping(0, 0, src_r, 0, 0, name_r);
2221        streaming.add_named_mapping(0, 0, src_s, 0, 0, name_s);
2222
2223        regular.add_mapping(0, 10, src_r, 0, 4);
2224        streaming.add_mapping(0, 10, src_s, 0, 4);
2225
2226        regular.add_mapping(1, 0, src_r, 1, 0);
2227        streaming.add_mapping(1, 0, src_s, 1, 0);
2228
2229        let sm_r = srcmap_sourcemap::SourceMap::from_json(&regular.to_json()).unwrap();
2230        let sm_s = srcmap_sourcemap::SourceMap::from_json(&streaming.to_json()).unwrap();
2231
2232        assert_eq!(sm_r.mapping_count(), sm_s.mapping_count());
2233        assert_eq!(sm_r.sources, sm_s.sources);
2234        assert_eq!(sm_r.names, sm_s.names);
2235        assert_eq!(sm_r.sources_content, sm_s.sources_content);
2236
2237        for (a, b) in sm_r.all_mappings().iter().zip(sm_s.all_mappings().iter()) {
2238            assert_eq!(a.generated_line, b.generated_line);
2239            assert_eq!(a.generated_column, b.generated_column);
2240            assert_eq!(a.source, b.source);
2241            assert_eq!(a.original_line, b.original_line);
2242            assert_eq!(a.original_column, b.original_column);
2243            assert_eq!(a.name, b.name);
2244        }
2245    }
2246
2247    #[test]
2248    fn streaming_to_decoded_map() {
2249        let mut sg = StreamingGenerator::new(None);
2250        let src = sg.add_source("test.js");
2251        sg.add_mapping(0, 0, src, 0, 0);
2252        sg.add_mapping(2, 5, src, 1, 3);
2253
2254        let sm = sg.to_decoded_map().unwrap();
2255        assert_eq!(sm.mapping_count(), 2);
2256        assert_eq!(sm.sources, vec!["test.js"]);
2257
2258        let loc = sm.original_position_for(2, 5).unwrap();
2259        assert_eq!(loc.line, 1);
2260        assert_eq!(loc.column, 3);
2261    }
2262
2263    #[test]
2264    fn streaming_source_dedup() {
2265        let mut sg = StreamingGenerator::new(None);
2266        let src1 = sg.add_source("a.js");
2267        let src2 = sg.add_source("a.js");
2268        assert_eq!(src1, src2);
2269        assert_eq!(sg.sources.len(), 1);
2270    }
2271
2272    #[test]
2273    fn streaming_ignore_list() {
2274        let mut sg = StreamingGenerator::new(None);
2275        let src = sg.add_source("vendor.js");
2276        sg.add_to_ignore_list(src);
2277        sg.add_mapping(0, 0, src, 0, 0);
2278
2279        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2280        assert_eq!(sm.ignore_list, vec![0]);
2281    }
2282
2283    #[test]
2284    fn streaming_empty() {
2285        let sg = StreamingGenerator::new(None);
2286        let json = sg.to_json();
2287        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2288        assert_eq!(sm.mapping_count(), 0);
2289    }
2290
2291    #[test]
2292    fn streaming_sparse_lines() {
2293        let mut sg = StreamingGenerator::new(None);
2294        let src = sg.add_source("a.js");
2295        sg.add_mapping(0, 0, src, 0, 0);
2296        sg.add_mapping(5, 0, src, 5, 0);
2297
2298        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2299        assert_eq!(sm.mapping_count(), 2);
2300        assert!(sm.original_position_for(0, 0).is_some());
2301        assert!(sm.original_position_for(5, 0).is_some());
2302    }
2303
2304    // ── Range mapping tests ───────────────────────────────────
2305
2306    #[test]
2307    fn range_mapping_basic() {
2308        let mut builder = SourceMapGenerator::new(None);
2309        let src = builder.add_source("input.js");
2310        builder.add_range_mapping(0, 0, src, 0, 0);
2311        builder.add_mapping(0, 5, src, 0, 10);
2312
2313        let json = builder.to_json();
2314        assert!(json.contains(r#""rangeMappings":"A""#));
2315    }
2316
2317    #[test]
2318    fn range_mapping_multiple_on_line() {
2319        let mut builder = SourceMapGenerator::new(None);
2320        let src = builder.add_source("input.js");
2321        builder.add_range_mapping(0, 0, src, 0, 0);
2322        builder.add_mapping(0, 5, src, 0, 10);
2323        builder.add_range_mapping(0, 10, src, 0, 20);
2324
2325        let json = builder.to_json();
2326        assert!(json.contains(r#""rangeMappings":"A,C""#));
2327    }
2328
2329    #[test]
2330    fn range_mapping_multi_line() {
2331        let mut builder = SourceMapGenerator::new(None);
2332        let src = builder.add_source("input.js");
2333        builder.add_range_mapping(0, 0, src, 0, 0);
2334        builder.add_range_mapping(1, 0, src, 1, 0);
2335
2336        let json = builder.to_json();
2337        assert!(json.contains(r#""rangeMappings":"A;A""#));
2338    }
2339
2340    #[test]
2341    fn no_range_mappings_omits_field() {
2342        let mut builder = SourceMapGenerator::new(None);
2343        let src = builder.add_source("input.js");
2344        builder.add_mapping(0, 0, src, 0, 0);
2345
2346        let json = builder.to_json();
2347        assert!(!json.contains("rangeMappings"));
2348    }
2349
2350    #[test]
2351    fn named_range_mapping() {
2352        let mut builder = SourceMapGenerator::new(None);
2353        let src = builder.add_source("input.js");
2354        let name = builder.add_name("foo");
2355        builder.add_named_range_mapping(0, 0, src, 0, 0, name);
2356
2357        let json = builder.to_json();
2358        assert!(json.contains(r#""rangeMappings":"A""#));
2359    }
2360
2361    #[test]
2362    fn to_decoded_map_preserves_range_mappings() {
2363        let mut builder = SourceMapGenerator::new(None);
2364        let src = builder.add_source("input.js");
2365        builder.add_range_mapping(0, 0, src, 0, 0);
2366        builder.add_mapping(0, 5, src, 0, 10);
2367
2368        let sm = builder.to_decoded_map();
2369        assert!(sm.has_range_mappings());
2370        let mappings = sm.all_mappings();
2371        assert!(mappings[0].is_range_mapping);
2372        assert!(!mappings[1].is_range_mapping);
2373    }
2374
2375    // ── Streaming range mapping tests ────────────────────────────
2376
2377    #[test]
2378    fn streaming_range_mapping_basic() {
2379        let mut sg = StreamingGenerator::new(None);
2380        let src = sg.add_source("input.js");
2381        sg.add_range_mapping(0, 0, src, 0, 0);
2382        sg.add_mapping(0, 5, src, 0, 10);
2383
2384        let json = sg.to_json();
2385        assert!(json.contains(r#""rangeMappings":"A""#));
2386    }
2387
2388    #[test]
2389    fn streaming_range_mapping_roundtrip() {
2390        let mut sg = StreamingGenerator::new(None);
2391        let src = sg.add_source("input.js");
2392        sg.add_range_mapping(0, 0, src, 0, 0);
2393        sg.add_mapping(0, 5, src, 0, 10);
2394
2395        let sm = sg.to_decoded_map().unwrap();
2396        assert!(sm.has_range_mappings());
2397        let mappings = sm.all_mappings();
2398        assert!(mappings[0].is_range_mapping);
2399        assert!(!mappings[1].is_range_mapping);
2400    }
2401
2402    #[test]
2403    fn streaming_range_and_named_range() {
2404        let mut sg = StreamingGenerator::new(None);
2405        let src = sg.add_source("input.js");
2406        let name = sg.add_name("foo");
2407        sg.add_range_mapping(0, 0, src, 0, 0);
2408        sg.add_named_range_mapping(0, 10, src, 0, 5, name);
2409
2410        let json = sg.to_json();
2411        assert!(json.contains(r#""rangeMappings":"A,B""#));
2412
2413        let sm = sg.to_decoded_map().unwrap();
2414        assert!(sm.has_range_mappings());
2415        let mappings = sm.all_mappings();
2416        assert!(mappings[0].is_range_mapping);
2417        assert!(mappings[1].is_range_mapping);
2418    }
2419
2420    #[test]
2421    fn streaming_range_mapping_matches_regular() {
2422        let mut regular = SourceMapGenerator::new(None);
2423        let mut streaming = StreamingGenerator::new(None);
2424
2425        let src_r = regular.add_source("input.js");
2426        let src_s = streaming.add_source("input.js");
2427
2428        regular.add_range_mapping(0, 0, src_r, 0, 0);
2429        streaming.add_range_mapping(0, 0, src_s, 0, 0);
2430
2431        regular.add_mapping(0, 5, src_r, 0, 10);
2432        streaming.add_mapping(0, 5, src_s, 0, 10);
2433
2434        regular.add_range_mapping(0, 10, src_r, 0, 20);
2435        streaming.add_range_mapping(0, 10, src_s, 0, 20);
2436
2437        regular.add_range_mapping(1, 0, src_r, 1, 0);
2438        streaming.add_range_mapping(1, 0, src_s, 1, 0);
2439
2440        let json_r = regular.to_json();
2441        let json_s = streaming.to_json();
2442
2443        let sm_r = srcmap_sourcemap::SourceMap::from_json(&json_r).unwrap();
2444        let sm_s = srcmap_sourcemap::SourceMap::from_json(&json_s).unwrap();
2445
2446        assert_eq!(sm_r.mapping_count(), sm_s.mapping_count());
2447
2448        for (a, b) in sm_r.all_mappings().iter().zip(sm_s.all_mappings().iter()) {
2449            assert_eq!(a.generated_line, b.generated_line);
2450            assert_eq!(a.generated_column, b.generated_column);
2451            assert_eq!(a.source, b.source);
2452            assert_eq!(a.original_line, b.original_line);
2453            assert_eq!(a.original_column, b.original_column);
2454            assert_eq!(a.name, b.name);
2455            assert_eq!(a.is_range_mapping, b.is_range_mapping);
2456        }
2457    }
2458}