Skip to main content

srcmap_scopes/
lib.rs

1//! Scopes and variables decoder/encoder for source maps (ECMA-426).
2//!
3//! Implements the "Scopes" proposal for source maps, enabling debuggers to
4//! reconstruct original scope trees, variable bindings, and inlined function
5//! call sites from generated code.
6//!
7//! # Examples
8//!
9//! ```
10//! use srcmap_scopes::{
11//!     decode_scopes, encode_scopes, Binding, CallSite, GeneratedRange,
12//!     OriginalScope, Position, ScopeInfo,
13//! };
14//!
15//! // Build scope info
16//! let info = ScopeInfo {
17//!     scopes: vec![Some(OriginalScope {
18//!         start: Position { line: 0, column: 0 },
19//!         end: Position { line: 5, column: 0 },
20//!         name: None,
21//!         kind: Some("global".to_string()),
22//!         is_stack_frame: false,
23//!         variables: vec!["x".to_string()],
24//!         children: vec![],
25//!     })],
26//!     ranges: vec![GeneratedRange {
27//!         start: Position { line: 0, column: 0 },
28//!         end: Position { line: 5, column: 0 },
29//!         is_stack_frame: false,
30//!         is_hidden: false,
31//!         definition: Some(0),
32//!         call_site: None,
33//!         bindings: vec![Binding::Expression("_x".to_string())],
34//!         children: vec![],
35//!     }],
36//! };
37//!
38//! // Encode
39//! let mut names = vec![];
40//! let encoded = encode_scopes(&info, &mut names);
41//! assert!(!encoded.is_empty());
42//!
43//! // Decode
44//! let decoded = decode_scopes(&encoded, &names, 1).unwrap();
45//! assert_eq!(decoded.scopes.len(), 1);
46//! assert!(decoded.scopes[0].is_some());
47//! ```
48
49mod decode;
50mod encode;
51
52use std::collections::HashMap;
53use std::fmt;
54
55pub use decode::decode_scopes;
56pub use encode::encode_scopes;
57
58use srcmap_codec::DecodeError;
59
60// ── Tag constants ────────────────────────────────────────────────
61
62const TAG_ORIGINAL_SCOPE_START: u64 = 0x1;
63const TAG_ORIGINAL_SCOPE_END: u64 = 0x2;
64const TAG_ORIGINAL_SCOPE_VARIABLES: u64 = 0x3;
65const TAG_GENERATED_RANGE_START: u64 = 0x4;
66const TAG_GENERATED_RANGE_END: u64 = 0x5;
67const TAG_GENERATED_RANGE_BINDINGS: u64 = 0x6;
68const TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS: u64 = 0x7;
69const TAG_GENERATED_RANGE_CALL_SITE: u64 = 0x8;
70
71// ── Flag constants ───────────────────────────────────────────────
72
73/// Flags for original scope start (B tag).
74const OS_FLAG_HAS_NAME: u64 = 0x1;
75const OS_FLAG_HAS_KIND: u64 = 0x2;
76const OS_FLAG_IS_STACK_FRAME: u64 = 0x4;
77
78/// Flags for generated range start (E tag).
79const GR_FLAG_HAS_LINE: u64 = 0x1;
80const GR_FLAG_HAS_DEFINITION: u64 = 0x2;
81const GR_FLAG_IS_STACK_FRAME: u64 = 0x4;
82const GR_FLAG_IS_HIDDEN: u64 = 0x8;
83
84// ── Public types ─────────────────────────────────────────────────
85
86/// A 0-based position in source code.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
88pub struct Position {
89    pub line: u32,
90    pub column: u32,
91}
92
93/// An original scope from authored source code.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct OriginalScope {
96    pub start: Position,
97    pub end: Position,
98    /// Scope name (e.g., function name). Stored in the `names` array.
99    pub name: Option<String>,
100    /// Scope kind (e.g., "global", "function", "block"). Stored in `names`.
101    pub kind: Option<String>,
102    /// Whether this scope is a stack frame (function boundary).
103    pub is_stack_frame: bool,
104    /// Variables declared in this scope.
105    pub variables: Vec<String>,
106    /// Child scopes nested within this one.
107    pub children: Vec<OriginalScope>,
108}
109
110/// A binding expression for a variable in a generated range.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum Binding {
113    /// Variable is available via this JS expression.
114    Expression(String),
115    /// Variable is not available in this range.
116    Unavailable,
117    /// Variable has different bindings in different sub-ranges.
118    SubRanges(Vec<SubRangeBinding>),
119}
120
121/// A sub-range binding within a generated range.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct SubRangeBinding {
124    /// The JS expression evaluating to the variable's value. `None` = unavailable.
125    pub expression: Option<String>,
126    /// Start position of this sub-range within the generated range.
127    pub from: Position,
128}
129
130/// A call site in original source code (for inlined functions).
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct CallSite {
133    pub source_index: u32,
134    pub line: u32,
135    pub column: u32,
136}
137
138/// A generated range in the output code.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct GeneratedRange {
141    pub start: Position,
142    pub end: Position,
143    /// Whether this range is a stack frame (function boundary).
144    pub is_stack_frame: bool,
145    /// Whether this stack frame should be hidden from traces.
146    pub is_hidden: bool,
147    /// Index into the pre-order list of all original scope starts.
148    pub definition: Option<u32>,
149    /// Call site if this range represents an inlined function body.
150    pub call_site: Option<CallSite>,
151    /// Variable bindings (one per variable in the referenced original scope).
152    pub bindings: Vec<Binding>,
153    /// Child ranges nested within this one.
154    pub children: Vec<GeneratedRange>,
155}
156
157/// Decoded scope information from a source map.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ScopeInfo {
160    /// Original scope trees, one per source file (aligned with `sources`).
161    /// `None` means no scope info for that source file.
162    pub scopes: Vec<Option<OriginalScope>>,
163    /// Top-level generated ranges for the output code.
164    pub ranges: Vec<GeneratedRange>,
165}
166
167impl ScopeInfo {
168    /// Get the original scope referenced by a generated range's `definition` index.
169    ///
170    /// The definition index references scopes in pre-order traversal order
171    /// across all source files.
172    pub fn original_scope_for_definition(&self, definition: u32) -> Option<&OriginalScope> {
173        let mut count = 0u32;
174        for scope in self.scopes.iter().flatten() {
175            if let Some(result) = find_nth_scope(scope, definition, &mut count) {
176                return Some(result);
177            }
178        }
179        None
180    }
181}
182
183fn find_nth_scope<'a>(
184    scope: &'a OriginalScope,
185    target: u32,
186    count: &mut u32,
187) -> Option<&'a OriginalScope> {
188    // Iterative pre-order traversal to avoid stack overflow on deeply nested scopes
189    let mut stack: Vec<&'a OriginalScope> = vec![scope];
190
191    while let Some(node) = stack.pop() {
192        if *count == target {
193            return Some(node);
194        }
195        *count += 1;
196        // Push children in reverse order so leftmost is processed first
197        for child in node.children.iter().rev() {
198            stack.push(child);
199        }
200    }
201    None
202}
203
204// ── Errors ───────────────────────────────────────────────────────
205
206/// Errors during scopes decoding.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum ScopesError {
209    /// VLQ decoding failed.
210    Vlq(DecodeError),
211    /// Scope end without matching scope start.
212    UnmatchedScopeEnd,
213    /// Scope was opened but never closed.
214    UnclosedScope,
215    /// Range end without matching range start.
216    UnmatchedRangeEnd,
217    /// Range was opened but never closed.
218    UnclosedRange,
219    /// Name index out of bounds.
220    InvalidNameIndex(i64),
221}
222
223impl fmt::Display for ScopesError {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self {
226            Self::Vlq(e) => write!(f, "VLQ decode error: {e}"),
227            Self::UnmatchedScopeEnd => write!(f, "scope end without matching start"),
228            Self::UnclosedScope => write!(f, "scope opened but never closed"),
229            Self::UnmatchedRangeEnd => write!(f, "range end without matching start"),
230            Self::UnclosedRange => write!(f, "range opened but never closed"),
231            Self::InvalidNameIndex(idx) => write!(f, "invalid name index: {idx}"),
232        }
233    }
234}
235
236impl std::error::Error for ScopesError {}
237
238impl From<DecodeError> for ScopesError {
239    fn from(e: DecodeError) -> Self {
240        Self::Vlq(e)
241    }
242}
243
244// ── Internal helpers ─────────────────────────────────────────────
245
246/// Resolve a name from the names array by absolute index.
247fn resolve_name(names: &[String], index: i64) -> Result<String, ScopesError> {
248    if index < 0 || index as usize >= names.len() {
249        return Err(ScopesError::InvalidNameIndex(index));
250    }
251    Ok(names[index as usize].clone())
252}
253
254/// Resolve a 1-based binding name (0 = unavailable).
255fn resolve_binding(names: &[String], index: u64) -> Result<Option<String>, ScopesError> {
256    if index == 0 {
257        return Ok(None);
258    }
259    let actual = (index - 1) as usize;
260    if actual >= names.len() {
261        return Err(ScopesError::InvalidNameIndex(index as i64));
262    }
263    Ok(Some(names[actual].clone()))
264}
265
266/// Look up or insert a name, returning its 0-based index.
267fn resolve_or_add_name(
268    name: &str,
269    names: &mut Vec<String>,
270    name_map: &mut HashMap<String, u32>,
271) -> u32 {
272    if let Some(&idx) = name_map.get(name) {
273        return idx;
274    }
275    let idx = names.len() as u32;
276    names.push(name.to_string());
277    name_map.insert(name.to_string(), idx);
278    idx
279}
280
281// ── Tests ────────────────────────────────────────────────────────
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn empty_scopes() {
289        let info = decode_scopes("", &[], 0).unwrap();
290        assert!(info.scopes.is_empty());
291        assert!(info.ranges.is_empty());
292    }
293
294    #[test]
295    fn empty_scopes_with_sources() {
296        // Two empty items (commas) for two source files with no scopes
297        let info = decode_scopes(",", &[], 2).unwrap();
298        assert_eq!(info.scopes.len(), 2);
299        assert!(info.scopes[0].is_none());
300        assert!(info.scopes[1].is_none());
301    }
302
303    #[test]
304    fn single_global_scope_roundtrip() {
305        let info = ScopeInfo {
306            scopes: vec![Some(OriginalScope {
307                start: Position { line: 0, column: 0 },
308                end: Position { line: 10, column: 0 },
309                name: None,
310                kind: Some("global".to_string()),
311                is_stack_frame: false,
312                variables: vec!["x".to_string(), "y".to_string()],
313                children: vec![],
314            })],
315            ranges: vec![],
316        };
317
318        let mut names = vec![];
319        let encoded = encode_scopes(&info, &mut names);
320        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
321
322        assert_eq!(decoded.scopes.len(), 1);
323        let scope = decoded.scopes[0].as_ref().unwrap();
324        assert_eq!(scope.start, Position { line: 0, column: 0 });
325        assert_eq!(scope.end, Position { line: 10, column: 0 });
326        assert_eq!(scope.kind.as_deref(), Some("global"));
327        assert_eq!(scope.name, None);
328        assert!(!scope.is_stack_frame);
329        assert_eq!(scope.variables, vec!["x", "y"]);
330    }
331
332    #[test]
333    fn nested_scopes_roundtrip() {
334        let info = ScopeInfo {
335            scopes: vec![Some(OriginalScope {
336                start: Position { line: 0, column: 0 },
337                end: Position { line: 10, column: 1 },
338                name: None,
339                kind: Some("global".to_string()),
340                is_stack_frame: false,
341                variables: vec!["z".to_string()],
342                children: vec![OriginalScope {
343                    start: Position { line: 1, column: 0 },
344                    end: Position { line: 5, column: 1 },
345                    name: Some("hello".to_string()),
346                    kind: Some("function".to_string()),
347                    is_stack_frame: true,
348                    variables: vec!["msg".to_string(), "result".to_string()],
349                    children: vec![],
350                }],
351            })],
352            ranges: vec![],
353        };
354
355        let mut names = vec![];
356        let encoded = encode_scopes(&info, &mut names);
357        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
358
359        let scope = decoded.scopes[0].as_ref().unwrap();
360        assert_eq!(scope.children.len(), 1);
361        let child = &scope.children[0];
362        assert_eq!(child.start, Position { line: 1, column: 0 });
363        assert_eq!(child.end, Position { line: 5, column: 1 });
364        assert_eq!(child.name.as_deref(), Some("hello"));
365        assert_eq!(child.kind.as_deref(), Some("function"));
366        assert!(child.is_stack_frame);
367        assert_eq!(child.variables, vec!["msg", "result"]);
368    }
369
370    #[test]
371    fn multiple_sources_with_gaps() {
372        let info = ScopeInfo {
373            scopes: vec![
374                Some(OriginalScope {
375                    start: Position { line: 0, column: 0 },
376                    end: Position { line: 5, column: 0 },
377                    name: None,
378                    kind: None,
379                    is_stack_frame: false,
380                    variables: vec![],
381                    children: vec![],
382                }),
383                None, // second source has no scopes
384                Some(OriginalScope {
385                    start: Position { line: 0, column: 0 },
386                    end: Position { line: 3, column: 0 },
387                    name: None,
388                    kind: None,
389                    is_stack_frame: false,
390                    variables: vec![],
391                    children: vec![],
392                }),
393            ],
394            ranges: vec![],
395        };
396
397        let mut names = vec![];
398        let encoded = encode_scopes(&info, &mut names);
399        let decoded = decode_scopes(&encoded, &names, 3).unwrap();
400
401        assert_eq!(decoded.scopes.len(), 3);
402        assert!(decoded.scopes[0].is_some());
403        assert!(decoded.scopes[1].is_none());
404        assert!(decoded.scopes[2].is_some());
405    }
406
407    #[test]
408    fn generated_ranges_roundtrip() {
409        let info = ScopeInfo {
410            scopes: vec![Some(OriginalScope {
411                start: Position { line: 0, column: 0 },
412                end: Position { line: 10, column: 0 },
413                name: None,
414                kind: Some("global".to_string()),
415                is_stack_frame: false,
416                variables: vec!["x".to_string()],
417                children: vec![],
418            })],
419            ranges: vec![GeneratedRange {
420                start: Position { line: 0, column: 0 },
421                end: Position { line: 10, column: 0 },
422                is_stack_frame: false,
423                is_hidden: false,
424                definition: Some(0),
425                call_site: None,
426                bindings: vec![Binding::Expression("_x".to_string())],
427                children: vec![],
428            }],
429        };
430
431        let mut names = vec![];
432        let encoded = encode_scopes(&info, &mut names);
433        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
434
435        assert_eq!(decoded.ranges.len(), 1);
436        let range = &decoded.ranges[0];
437        assert_eq!(range.start, Position { line: 0, column: 0 });
438        assert_eq!(range.end, Position { line: 10, column: 0 });
439        assert_eq!(range.definition, Some(0));
440        assert_eq!(range.bindings, vec![Binding::Expression("_x".to_string())]);
441    }
442
443    #[test]
444    fn nested_ranges_with_inlining() {
445        let info = ScopeInfo {
446            scopes: vec![Some(OriginalScope {
447                start: Position { line: 0, column: 0 },
448                end: Position { line: 10, column: 0 },
449                name: None,
450                kind: Some("global".to_string()),
451                is_stack_frame: false,
452                variables: vec!["x".to_string()],
453                children: vec![OriginalScope {
454                    start: Position { line: 1, column: 0 },
455                    end: Position { line: 5, column: 1 },
456                    name: Some("fn1".to_string()),
457                    kind: Some("function".to_string()),
458                    is_stack_frame: true,
459                    variables: vec!["a".to_string()],
460                    children: vec![],
461                }],
462            })],
463            ranges: vec![GeneratedRange {
464                start: Position { line: 0, column: 0 },
465                end: Position { line: 10, column: 0 },
466                is_stack_frame: false,
467                is_hidden: false,
468                definition: Some(0),
469                call_site: None,
470                bindings: vec![Binding::Expression("_x".to_string())],
471                children: vec![GeneratedRange {
472                    start: Position { line: 6, column: 0 },
473                    end: Position { line: 8, column: 20 },
474                    is_stack_frame: true,
475                    is_hidden: false,
476                    definition: Some(1),
477                    call_site: Some(CallSite { source_index: 0, line: 7, column: 0 }),
478                    bindings: vec![Binding::Expression("\"hello\"".to_string())],
479                    children: vec![],
480                }],
481            }],
482        };
483
484        let mut names = vec![];
485        let encoded = encode_scopes(&info, &mut names);
486        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
487
488        assert_eq!(decoded.ranges.len(), 1);
489        let outer = &decoded.ranges[0];
490        assert_eq!(outer.children.len(), 1);
491        let inner = &outer.children[0];
492        assert!(inner.is_stack_frame);
493        assert_eq!(inner.definition, Some(1));
494        assert_eq!(inner.call_site, Some(CallSite { source_index: 0, line: 7, column: 0 }));
495        assert_eq!(inner.bindings, vec![Binding::Expression("\"hello\"".to_string())]);
496    }
497
498    #[test]
499    fn unavailable_bindings() {
500        let info = ScopeInfo {
501            scopes: vec![Some(OriginalScope {
502                start: Position { line: 0, column: 0 },
503                end: Position { line: 5, column: 0 },
504                name: None,
505                kind: None,
506                is_stack_frame: false,
507                variables: vec!["a".to_string(), "b".to_string()],
508                children: vec![],
509            })],
510            ranges: vec![GeneratedRange {
511                start: Position { line: 0, column: 0 },
512                end: Position { line: 5, column: 0 },
513                is_stack_frame: false,
514                is_hidden: false,
515                definition: Some(0),
516                call_site: None,
517                bindings: vec![Binding::Expression("_a".to_string()), Binding::Unavailable],
518                children: vec![],
519            }],
520        };
521
522        let mut names = vec![];
523        let encoded = encode_scopes(&info, &mut names);
524        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
525
526        assert_eq!(
527            decoded.ranges[0].bindings,
528            vec![Binding::Expression("_a".to_string()), Binding::Unavailable,]
529        );
530    }
531
532    #[test]
533    fn sub_range_bindings_roundtrip() {
534        let info = ScopeInfo {
535            scopes: vec![Some(OriginalScope {
536                start: Position { line: 0, column: 0 },
537                end: Position { line: 20, column: 0 },
538                name: None,
539                kind: None,
540                is_stack_frame: false,
541                variables: vec!["x".to_string(), "y".to_string()],
542                children: vec![],
543            })],
544            ranges: vec![GeneratedRange {
545                start: Position { line: 0, column: 0 },
546                end: Position { line: 20, column: 0 },
547                is_stack_frame: false,
548                is_hidden: false,
549                definition: Some(0),
550                call_site: None,
551                bindings: vec![
552                    Binding::SubRanges(vec![
553                        SubRangeBinding {
554                            expression: Some("a".to_string()),
555                            from: Position { line: 0, column: 0 },
556                        },
557                        SubRangeBinding {
558                            expression: Some("b".to_string()),
559                            from: Position { line: 5, column: 0 },
560                        },
561                        SubRangeBinding {
562                            expression: None,
563                            from: Position { line: 10, column: 0 },
564                        },
565                    ]),
566                    Binding::Expression("_y".to_string()),
567                ],
568                children: vec![],
569            }],
570        };
571
572        let mut names = vec![];
573        let encoded = encode_scopes(&info, &mut names);
574        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
575
576        let bindings = &decoded.ranges[0].bindings;
577        assert_eq!(bindings.len(), 2);
578
579        match &bindings[0] {
580            Binding::SubRanges(subs) => {
581                assert_eq!(subs.len(), 3);
582                assert_eq!(subs[0].expression.as_deref(), Some("a"));
583                assert_eq!(subs[0].from, Position { line: 0, column: 0 });
584                assert_eq!(subs[1].expression.as_deref(), Some("b"));
585                assert_eq!(subs[1].from, Position { line: 5, column: 0 });
586                assert_eq!(subs[2].expression, None);
587                assert_eq!(subs[2].from, Position { line: 10, column: 0 });
588            }
589            other => panic!("expected SubRanges, got {other:?}"),
590        }
591        assert_eq!(bindings[1], Binding::Expression("_y".to_string()));
592    }
593
594    #[test]
595    fn hidden_range() {
596        let info = ScopeInfo {
597            scopes: vec![Some(OriginalScope {
598                start: Position { line: 0, column: 0 },
599                end: Position { line: 5, column: 0 },
600                name: None,
601                kind: None,
602                is_stack_frame: false,
603                variables: vec![],
604                children: vec![],
605            })],
606            ranges: vec![GeneratedRange {
607                start: Position { line: 0, column: 0 },
608                end: Position { line: 5, column: 0 },
609                is_stack_frame: true,
610                is_hidden: true,
611                definition: Some(0),
612                call_site: None,
613                bindings: vec![],
614                children: vec![],
615            }],
616        };
617
618        let mut names = vec![];
619        let encoded = encode_scopes(&info, &mut names);
620        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
621
622        assert!(decoded.ranges[0].is_stack_frame);
623        assert!(decoded.ranges[0].is_hidden);
624    }
625
626    #[test]
627    fn definition_resolution() {
628        let info = ScopeInfo {
629            scopes: vec![Some(OriginalScope {
630                start: Position { line: 0, column: 0 },
631                end: Position { line: 10, column: 0 },
632                name: None,
633                kind: Some("global".to_string()),
634                is_stack_frame: false,
635                variables: vec![],
636                children: vec![
637                    OriginalScope {
638                        start: Position { line: 1, column: 0 },
639                        end: Position { line: 4, column: 1 },
640                        name: Some("foo".to_string()),
641                        kind: Some("function".to_string()),
642                        is_stack_frame: true,
643                        variables: vec![],
644                        children: vec![],
645                    },
646                    OriginalScope {
647                        start: Position { line: 5, column: 0 },
648                        end: Position { line: 9, column: 1 },
649                        name: Some("bar".to_string()),
650                        kind: Some("function".to_string()),
651                        is_stack_frame: true,
652                        variables: vec![],
653                        children: vec![],
654                    },
655                ],
656            })],
657            ranges: vec![],
658        };
659
660        // Definition 0 = global scope
661        let scope0 = info.original_scope_for_definition(0).unwrap();
662        assert_eq!(scope0.kind.as_deref(), Some("global"));
663
664        // Definition 1 = foo
665        let scope1 = info.original_scope_for_definition(1).unwrap();
666        assert_eq!(scope1.name.as_deref(), Some("foo"));
667
668        // Definition 2 = bar
669        let scope2 = info.original_scope_for_definition(2).unwrap();
670        assert_eq!(scope2.name.as_deref(), Some("bar"));
671
672        // Definition 3 = out of bounds
673        assert!(info.original_scope_for_definition(3).is_none());
674    }
675
676    #[test]
677    fn scopes_only_no_ranges() {
678        let info = ScopeInfo {
679            scopes: vec![Some(OriginalScope {
680                start: Position { line: 0, column: 0 },
681                end: Position { line: 5, column: 0 },
682                name: None,
683                kind: None,
684                is_stack_frame: false,
685                variables: vec![],
686                children: vec![],
687            })],
688            ranges: vec![],
689        };
690
691        let mut names = vec![];
692        let encoded = encode_scopes(&info, &mut names);
693        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
694
695        assert_eq!(decoded.scopes.len(), 1);
696        assert!(decoded.scopes[0].is_some());
697        assert!(decoded.ranges.is_empty());
698    }
699
700    #[test]
701    fn ranges_only_no_scopes() {
702        let info = ScopeInfo {
703            scopes: vec![None],
704            ranges: vec![GeneratedRange {
705                start: Position { line: 0, column: 0 },
706                end: Position { line: 5, column: 0 },
707                is_stack_frame: false,
708                is_hidden: false,
709                definition: None,
710                call_site: None,
711                bindings: vec![],
712                children: vec![],
713            }],
714        };
715
716        let mut names = vec![];
717        let encoded = encode_scopes(&info, &mut names);
718        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
719
720        assert_eq!(decoded.scopes.len(), 1);
721        assert!(decoded.scopes[0].is_none());
722        assert_eq!(decoded.ranges.len(), 1);
723    }
724
725    #[test]
726    fn range_no_definition() {
727        let info = ScopeInfo {
728            scopes: vec![],
729            ranges: vec![GeneratedRange {
730                start: Position { line: 0, column: 0 },
731                end: Position { line: 5, column: 0 },
732                is_stack_frame: false,
733                is_hidden: false,
734                definition: None,
735                call_site: None,
736                bindings: vec![],
737                children: vec![],
738            }],
739        };
740
741        let mut names = vec![];
742        let encoded = encode_scopes(&info, &mut names);
743        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
744
745        assert_eq!(decoded.ranges.len(), 1);
746        assert_eq!(decoded.ranges[0].definition, None);
747    }
748
749    #[test]
750    fn scopes_error_display() {
751        let err = ScopesError::UnmatchedScopeEnd;
752        assert_eq!(err.to_string(), "scope end without matching start");
753
754        let err = ScopesError::UnclosedScope;
755        assert_eq!(err.to_string(), "scope opened but never closed");
756
757        let err = ScopesError::UnmatchedRangeEnd;
758        assert_eq!(err.to_string(), "range end without matching start");
759
760        let err = ScopesError::UnclosedRange;
761        assert_eq!(err.to_string(), "range opened but never closed");
762
763        let err = ScopesError::InvalidNameIndex(42);
764        assert_eq!(err.to_string(), "invalid name index: 42");
765
766        let vlq_err = srcmap_codec::DecodeError::UnexpectedEof { offset: 5 };
767        let err = ScopesError::Vlq(vlq_err);
768        assert!(err.to_string().contains("VLQ decode error"));
769    }
770
771    #[test]
772    fn scopes_error_from_decode_error() {
773        let vlq_err = srcmap_codec::DecodeError::UnexpectedEof { offset: 0 };
774        let err: ScopesError = vlq_err.into();
775        assert!(matches!(err, ScopesError::Vlq(_)));
776    }
777
778    #[test]
779    fn invalid_name_index_error() {
780        // Encode scopes that reference name index 0, but pass empty names array to decode
781        let info = ScopeInfo {
782            scopes: vec![Some(OriginalScope {
783                start: Position { line: 0, column: 0 },
784                end: Position { line: 5, column: 0 },
785                name: Some("test".to_string()),
786                kind: None,
787                is_stack_frame: false,
788                variables: vec![],
789                children: vec![],
790            })],
791            ranges: vec![],
792        };
793
794        let mut names = vec![];
795        let encoded = encode_scopes(&info, &mut names);
796        // Now decode with empty names - should fail with InvalidNameIndex
797        let err = decode_scopes(&encoded, &[], 1).unwrap_err();
798        assert!(matches!(err, ScopesError::InvalidNameIndex(_)));
799    }
800
801    #[test]
802    fn invalid_binding_index_error() {
803        // Create a range with a binding expression that requires name index 0
804        let info = ScopeInfo {
805            scopes: vec![Some(OriginalScope {
806                start: Position { line: 0, column: 0 },
807                end: Position { line: 5, column: 0 },
808                name: None,
809                kind: None,
810                is_stack_frame: false,
811                variables: vec!["x".to_string()],
812                children: vec![],
813            })],
814            ranges: vec![GeneratedRange {
815                start: Position { line: 0, column: 0 },
816                end: Position { line: 5, column: 0 },
817                is_stack_frame: false,
818                is_hidden: false,
819                definition: Some(0),
820                call_site: None,
821                bindings: vec![Binding::Expression("_x".to_string())],
822                children: vec![],
823            }],
824        };
825
826        let mut names = vec![];
827        let encoded = encode_scopes(&info, &mut names);
828        // Decode with truncated names (remove the binding expression name)
829        let short_names: Vec<String> = names.iter().take(1).cloned().collect();
830        let err = decode_scopes(&encoded, &short_names, 1).unwrap_err();
831        assert!(matches!(err, ScopesError::InvalidNameIndex(_)));
832    }
833
834    #[test]
835    fn scope_same_line_end() {
836        // Scope that starts and ends on the same line (column relative)
837        let info = ScopeInfo {
838            scopes: vec![Some(OriginalScope {
839                start: Position { line: 5, column: 10 },
840                end: Position { line: 5, column: 30 },
841                name: None,
842                kind: None,
843                is_stack_frame: false,
844                variables: vec![],
845                children: vec![],
846            })],
847            ranges: vec![],
848        };
849
850        let mut names = vec![];
851        let encoded = encode_scopes(&info, &mut names);
852        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
853
854        let scope = decoded.scopes[0].as_ref().unwrap();
855        assert_eq!(scope.start, Position { line: 5, column: 10 });
856        assert_eq!(scope.end, Position { line: 5, column: 30 });
857    }
858
859    #[test]
860    fn range_same_line() {
861        // Range that starts and ends on the same line
862        let info = ScopeInfo {
863            scopes: vec![Some(OriginalScope {
864                start: Position { line: 0, column: 0 },
865                end: Position { line: 10, column: 0 },
866                name: None,
867                kind: None,
868                is_stack_frame: false,
869                variables: vec![],
870                children: vec![],
871            })],
872            ranges: vec![GeneratedRange {
873                start: Position { line: 3, column: 5 },
874                end: Position { line: 3, column: 25 },
875                is_stack_frame: false,
876                is_hidden: false,
877                definition: Some(0),
878                call_site: None,
879                bindings: vec![],
880                children: vec![],
881            }],
882        };
883
884        let mut names = vec![];
885        let encoded = encode_scopes(&info, &mut names);
886        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
887
888        let range = &decoded.ranges[0];
889        assert_eq!(range.start, Position { line: 3, column: 5 });
890        assert_eq!(range.end, Position { line: 3, column: 25 });
891    }
892
893    #[test]
894    fn scopes_first_empty_second_populated() {
895        let info = ScopeInfo {
896            scopes: vec![
897                None, // First source has no scopes
898                Some(OriginalScope {
899                    start: Position { line: 0, column: 0 },
900                    end: Position { line: 5, column: 0 },
901                    name: None,
902                    kind: None,
903                    is_stack_frame: false,
904                    variables: vec![],
905                    children: vec![],
906                }),
907            ],
908            ranges: vec![],
909        };
910
911        let mut names = vec![];
912        let encoded = encode_scopes(&info, &mut names);
913        let decoded = decode_scopes(&encoded, &names, 2).unwrap();
914
915        assert!(decoded.scopes[0].is_none());
916        assert!(decoded.scopes[1].is_some());
917    }
918
919    #[test]
920    fn ranges_only_no_scopes_multi_source() {
921        let info = ScopeInfo {
922            scopes: vec![None, None],
923            ranges: vec![GeneratedRange {
924                start: Position { line: 0, column: 0 },
925                end: Position { line: 5, column: 0 },
926                is_stack_frame: false,
927                is_hidden: false,
928                definition: None,
929                call_site: None,
930                bindings: vec![],
931                children: vec![],
932            }],
933        };
934
935        let mut names = vec![];
936        let encoded = encode_scopes(&info, &mut names);
937        let decoded = decode_scopes(&encoded, &names, 2).unwrap();
938
939        assert_eq!(decoded.scopes.len(), 2);
940        assert!(decoded.scopes[0].is_none());
941        assert!(decoded.scopes[1].is_none());
942        assert_eq!(decoded.ranges.len(), 1);
943    }
944
945    #[test]
946    fn range_no_definition_explicit() {
947        let info = ScopeInfo {
948            scopes: vec![None],
949            ranges: vec![GeneratedRange {
950                start: Position { line: 0, column: 0 },
951                end: Position { line: 5, column: 0 },
952                is_stack_frame: false,
953                is_hidden: false,
954                definition: None,
955                call_site: None,
956                bindings: vec![],
957                children: vec![],
958            }],
959        };
960
961        let mut names = vec![];
962        let encoded = encode_scopes(&info, &mut names);
963        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
964
965        assert_eq!(decoded.ranges[0].definition, None);
966    }
967
968    #[test]
969    fn sub_range_same_line_bindings() {
970        // Sub-ranges where positions are on the same line (column-only delta)
971        let info = ScopeInfo {
972            scopes: vec![Some(OriginalScope {
973                start: Position { line: 0, column: 0 },
974                end: Position { line: 10, column: 0 },
975                name: None,
976                kind: None,
977                is_stack_frame: false,
978                variables: vec!["x".to_string()],
979                children: vec![],
980            })],
981            ranges: vec![GeneratedRange {
982                start: Position { line: 0, column: 0 },
983                end: Position { line: 10, column: 0 },
984                is_stack_frame: false,
985                is_hidden: false,
986                definition: Some(0),
987                call_site: None,
988                bindings: vec![Binding::SubRanges(vec![
989                    SubRangeBinding {
990                        expression: Some("a".to_string()),
991                        from: Position { line: 0, column: 0 },
992                    },
993                    SubRangeBinding {
994                        expression: Some("b".to_string()),
995                        from: Position { line: 0, column: 15 },
996                    },
997                ])],
998                children: vec![],
999            }],
1000        };
1001
1002        let mut names = vec![];
1003        let encoded = encode_scopes(&info, &mut names);
1004        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1005
1006        match &decoded.ranges[0].bindings[0] {
1007            Binding::SubRanges(subs) => {
1008                assert_eq!(subs.len(), 2);
1009                assert_eq!(subs[0].from, Position { line: 0, column: 0 });
1010                assert_eq!(subs[1].from, Position { line: 0, column: 15 });
1011            }
1012            other => panic!("expected SubRanges, got {other:?}"),
1013        }
1014    }
1015
1016    #[test]
1017    fn call_site_with_nonzero_values() {
1018        let info = ScopeInfo {
1019            scopes: vec![Some(OriginalScope {
1020                start: Position { line: 0, column: 0 },
1021                end: Position { line: 20, column: 0 },
1022                name: None,
1023                kind: None,
1024                is_stack_frame: false,
1025                variables: vec![],
1026                children: vec![],
1027            })],
1028            ranges: vec![GeneratedRange {
1029                start: Position { line: 0, column: 0 },
1030                end: Position { line: 20, column: 0 },
1031                is_stack_frame: false,
1032                is_hidden: false,
1033                definition: Some(0),
1034                call_site: Some(CallSite { source_index: 2, line: 15, column: 8 }),
1035                bindings: vec![],
1036                children: vec![],
1037            }],
1038        };
1039
1040        let mut names = vec![];
1041        let encoded = encode_scopes(&info, &mut names);
1042        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1043
1044        let cs = decoded.ranges[0].call_site.as_ref().unwrap();
1045        assert_eq!(cs.source_index, 2);
1046        assert_eq!(cs.line, 15);
1047        assert_eq!(cs.column, 8);
1048    }
1049
1050    // ── Additional coverage tests ────────────────────────────────
1051
1052    #[test]
1053    fn scope_with_name_and_kind_roundtrip() {
1054        // Exercises OS_FLAG_HAS_NAME + OS_FLAG_HAS_KIND together
1055        // Covers decode.rs lines 139, 141, 143, 151, 159, 161
1056        let info = ScopeInfo {
1057            scopes: vec![Some(OriginalScope {
1058                start: Position { line: 2, column: 4 },
1059                end: Position { line: 15, column: 1 },
1060                name: Some("myFunc".to_string()),
1061                kind: Some("function".to_string()),
1062                is_stack_frame: true,
1063                variables: vec!["arg1".to_string(), "arg2".to_string()],
1064                children: vec![OriginalScope {
1065                    start: Position { line: 3, column: 8 },
1066                    end: Position { line: 14, column: 5 },
1067                    name: Some("innerBlock".to_string()),
1068                    kind: Some("block".to_string()),
1069                    is_stack_frame: false,
1070                    variables: vec!["tmp".to_string()],
1071                    children: vec![],
1072                }],
1073            })],
1074            ranges: vec![],
1075        };
1076
1077        let mut names = vec![];
1078        let encoded = encode_scopes(&info, &mut names);
1079        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1080
1081        let scope = decoded.scopes[0].as_ref().unwrap();
1082        assert_eq!(scope.name.as_deref(), Some("myFunc"));
1083        assert_eq!(scope.kind.as_deref(), Some("function"));
1084        assert!(scope.is_stack_frame);
1085        assert_eq!(scope.variables, vec!["arg1", "arg2"]);
1086
1087        let child = &scope.children[0];
1088        assert_eq!(child.name.as_deref(), Some("innerBlock"));
1089        assert_eq!(child.kind.as_deref(), Some("block"));
1090        assert!(!child.is_stack_frame);
1091        assert_eq!(child.variables, vec!["tmp"]);
1092    }
1093
1094    #[test]
1095    fn range_end_multiline_2vlq() {
1096        // Range where end is on a different line than start, producing a
1097        // 2-VLQ range end (line_delta + column).
1098        // Covers decode.rs lines 282, 286, 288 (TAG_GENERATED_RANGE_END with 2 VLQs)
1099        let info = ScopeInfo {
1100            scopes: vec![],
1101            ranges: vec![GeneratedRange {
1102                start: Position { line: 0, column: 0 },
1103                end: Position { line: 7, column: 15 },
1104                is_stack_frame: false,
1105                is_hidden: false,
1106                definition: None,
1107                call_site: None,
1108                bindings: vec![],
1109                children: vec![GeneratedRange {
1110                    start: Position { line: 1, column: 5 },
1111                    end: Position { line: 4, column: 10 },
1112                    is_stack_frame: false,
1113                    is_hidden: false,
1114                    definition: None,
1115                    call_site: None,
1116                    bindings: vec![],
1117                    children: vec![],
1118                }],
1119            }],
1120        };
1121
1122        let mut names = vec![];
1123        let encoded = encode_scopes(&info, &mut names);
1124        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
1125
1126        let outer = &decoded.ranges[0];
1127        assert_eq!(outer.end, Position { line: 7, column: 15 });
1128        let inner = &outer.children[0];
1129        assert_eq!(inner.start, Position { line: 1, column: 5 });
1130        assert_eq!(inner.end, Position { line: 4, column: 10 });
1131    }
1132
1133    #[test]
1134    fn binding_unavailable_roundtrip() {
1135        // Exercises Binding::Unavailable (binding idx = 0) path
1136        // Covers decode.rs lines 333, 340 (TAG_GENERATED_RANGE_BINDINGS with idx=0)
1137        let info = ScopeInfo {
1138            scopes: vec![Some(OriginalScope {
1139                start: Position { line: 0, column: 0 },
1140                end: Position { line: 10, column: 0 },
1141                name: None,
1142                kind: None,
1143                is_stack_frame: false,
1144                variables: vec!["a".to_string(), "b".to_string(), "c".to_string()],
1145                children: vec![],
1146            })],
1147            ranges: vec![GeneratedRange {
1148                start: Position { line: 0, column: 0 },
1149                end: Position { line: 10, column: 0 },
1150                is_stack_frame: false,
1151                is_hidden: false,
1152                definition: Some(0),
1153                call_site: None,
1154                bindings: vec![
1155                    Binding::Unavailable,
1156                    Binding::Expression("_b".to_string()),
1157                    Binding::Unavailable,
1158                ],
1159                children: vec![],
1160            }],
1161        };
1162
1163        let mut names = vec![];
1164        let encoded = encode_scopes(&info, &mut names);
1165        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1166
1167        assert_eq!(decoded.ranges[0].bindings.len(), 3);
1168        assert_eq!(decoded.ranges[0].bindings[0], Binding::Unavailable);
1169        assert_eq!(decoded.ranges[0].bindings[1], Binding::Expression("_b".to_string()));
1170        assert_eq!(decoded.ranges[0].bindings[2], Binding::Unavailable);
1171    }
1172
1173    #[test]
1174    fn sub_range_with_none_expression() {
1175        // Sub-range bindings where a sub-range has expression = None (Unavailable)
1176        // Covers encode.rs lines 267, 271 (None expression → emit 0)
1177        // and decode.rs lines 353, 354, 357, 364 (sub-range reading)
1178        let info = ScopeInfo {
1179            scopes: vec![Some(OriginalScope {
1180                start: Position { line: 0, column: 0 },
1181                end: Position { line: 20, column: 0 },
1182                name: None,
1183                kind: None,
1184                is_stack_frame: false,
1185                variables: vec!["x".to_string()],
1186                children: vec![],
1187            })],
1188            ranges: vec![GeneratedRange {
1189                start: Position { line: 0, column: 0 },
1190                end: Position { line: 20, column: 0 },
1191                is_stack_frame: false,
1192                is_hidden: false,
1193                definition: Some(0),
1194                call_site: None,
1195                bindings: vec![Binding::SubRanges(vec![
1196                    SubRangeBinding {
1197                        expression: Some("a".to_string()),
1198                        from: Position { line: 0, column: 0 },
1199                    },
1200                    SubRangeBinding { expression: None, from: Position { line: 5, column: 0 } },
1201                    SubRangeBinding {
1202                        expression: Some("c".to_string()),
1203                        from: Position { line: 10, column: 0 },
1204                    },
1205                ])],
1206                children: vec![],
1207            }],
1208        };
1209
1210        let mut names = vec![];
1211        let encoded = encode_scopes(&info, &mut names);
1212        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1213
1214        match &decoded.ranges[0].bindings[0] {
1215            Binding::SubRanges(subs) => {
1216                assert_eq!(subs.len(), 3);
1217                assert_eq!(subs[0].expression.as_deref(), Some("a"));
1218                assert_eq!(subs[1].expression, None);
1219                assert_eq!(subs[2].expression.as_deref(), Some("c"));
1220            }
1221            other => panic!("expected SubRanges, got {other:?}"),
1222        }
1223    }
1224
1225    #[test]
1226    fn sub_range_multiple_variables() {
1227        // Multiple variables each with sub-ranges, exercises the H tag
1228        // encode/decode with h_first handling (encode.rs line 290)
1229        // and decode.rs lines 353-375 (TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS)
1230        let info = ScopeInfo {
1231            scopes: vec![Some(OriginalScope {
1232                start: Position { line: 0, column: 0 },
1233                end: Position { line: 30, column: 0 },
1234                name: None,
1235                kind: None,
1236                is_stack_frame: false,
1237                variables: vec!["x".to_string(), "y".to_string(), "z".to_string()],
1238                children: vec![],
1239            })],
1240            ranges: vec![GeneratedRange {
1241                start: Position { line: 0, column: 0 },
1242                end: Position { line: 30, column: 0 },
1243                is_stack_frame: false,
1244                is_hidden: false,
1245                definition: Some(0),
1246                call_site: None,
1247                bindings: vec![
1248                    // Variable 0 (x): sub-ranges
1249                    Binding::SubRanges(vec![
1250                        SubRangeBinding {
1251                            expression: Some("_x1".to_string()),
1252                            from: Position { line: 0, column: 0 },
1253                        },
1254                        SubRangeBinding {
1255                            expression: Some("_x2".to_string()),
1256                            from: Position { line: 10, column: 0 },
1257                        },
1258                    ]),
1259                    // Variable 1 (y): simple binding
1260                    Binding::Expression("_y".to_string()),
1261                    // Variable 2 (z): sub-ranges (second H item, exercises h_first=false)
1262                    Binding::SubRanges(vec![
1263                        SubRangeBinding {
1264                            expression: Some("_z1".to_string()),
1265                            from: Position { line: 0, column: 0 },
1266                        },
1267                        SubRangeBinding {
1268                            expression: None,
1269                            from: Position { line: 15, column: 5 },
1270                        },
1271                        SubRangeBinding {
1272                            expression: Some("_z3".to_string()),
1273                            from: Position { line: 20, column: 0 },
1274                        },
1275                    ]),
1276                ],
1277                children: vec![],
1278            }],
1279        };
1280
1281        let mut names = vec![];
1282        let encoded = encode_scopes(&info, &mut names);
1283        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1284
1285        let bindings = &decoded.ranges[0].bindings;
1286        assert_eq!(bindings.len(), 3);
1287
1288        // Variable 0: sub-ranges
1289        match &bindings[0] {
1290            Binding::SubRanges(subs) => {
1291                assert_eq!(subs.len(), 2);
1292                assert_eq!(subs[0].expression.as_deref(), Some("_x1"));
1293                assert_eq!(subs[1].expression.as_deref(), Some("_x2"));
1294                assert_eq!(subs[1].from, Position { line: 10, column: 0 });
1295            }
1296            other => panic!("expected SubRanges for x, got {other:?}"),
1297        }
1298
1299        // Variable 1: simple expression
1300        assert_eq!(bindings[1], Binding::Expression("_y".to_string()));
1301
1302        // Variable 2: sub-ranges (second H item)
1303        match &bindings[2] {
1304            Binding::SubRanges(subs) => {
1305                assert_eq!(subs.len(), 3);
1306                assert_eq!(subs[0].expression.as_deref(), Some("_z1"));
1307                assert_eq!(subs[1].expression, None);
1308                assert_eq!(subs[1].from, Position { line: 15, column: 5 });
1309                assert_eq!(subs[2].expression.as_deref(), Some("_z3"));
1310            }
1311            other => panic!("expected SubRanges for z, got {other:?}"),
1312        }
1313    }
1314
1315    #[test]
1316    fn call_site_on_standalone_range() {
1317        // Range with call_site but also a definition (typical inlining pattern).
1318        // Exercises decode.rs lines 380-388 (TAG_GENERATED_RANGE_CALL_SITE)
1319        let info = ScopeInfo {
1320            scopes: vec![Some(OriginalScope {
1321                start: Position { line: 0, column: 0 },
1322                end: Position { line: 30, column: 0 },
1323                name: None,
1324                kind: Some("global".to_string()),
1325                is_stack_frame: false,
1326                variables: vec![],
1327                children: vec![OriginalScope {
1328                    start: Position { line: 5, column: 0 },
1329                    end: Position { line: 10, column: 1 },
1330                    name: Some("inlined".to_string()),
1331                    kind: Some("function".to_string()),
1332                    is_stack_frame: true,
1333                    variables: vec!["p".to_string()],
1334                    children: vec![],
1335                }],
1336            })],
1337            ranges: vec![GeneratedRange {
1338                start: Position { line: 0, column: 0 },
1339                end: Position { line: 30, column: 0 },
1340                is_stack_frame: false,
1341                is_hidden: false,
1342                definition: Some(0),
1343                call_site: None,
1344                bindings: vec![],
1345                children: vec![GeneratedRange {
1346                    start: Position { line: 12, column: 0 },
1347                    end: Position { line: 18, column: 0 },
1348                    is_stack_frame: true,
1349                    is_hidden: false,
1350                    definition: Some(1),
1351                    call_site: Some(CallSite { source_index: 0, line: 20, column: 4 }),
1352                    bindings: vec![Binding::Expression("arg0".to_string())],
1353                    children: vec![],
1354                }],
1355            }],
1356        };
1357
1358        let mut names = vec![];
1359        let encoded = encode_scopes(&info, &mut names);
1360        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1361
1362        let inner = &decoded.ranges[0].children[0];
1363        assert!(inner.is_stack_frame);
1364        assert_eq!(inner.definition, Some(1));
1365        let cs = inner.call_site.as_ref().unwrap();
1366        assert_eq!(cs.source_index, 0);
1367        assert_eq!(cs.line, 20);
1368        assert_eq!(cs.column, 4);
1369        assert_eq!(inner.bindings, vec![Binding::Expression("arg0".to_string())]);
1370    }
1371
1372    #[test]
1373    fn unknown_tag_skipped() {
1374        // Manually craft a string with an unknown tag (e.g., tag 0x9) followed
1375        // by some VLQs, then a valid scope.
1376        // This exercises decode.rs lines 393, 394 (unknown tag skip).
1377        //
1378        // First encode a simple scope, then prepend an unknown tag item.
1379        let info = ScopeInfo {
1380            scopes: vec![Some(OriginalScope {
1381                start: Position { line: 0, column: 0 },
1382                end: Position { line: 5, column: 0 },
1383                name: None,
1384                kind: None,
1385                is_stack_frame: false,
1386                variables: vec![],
1387                children: vec![],
1388            })],
1389            ranges: vec![],
1390        };
1391
1392        let mut names = vec![];
1393        let encoded = encode_scopes(&info, &mut names);
1394
1395        // Build unknown tag item: tag 0x12 (= 18, unused) followed by two
1396        // unsigned VLQs, then comma, then the valid encoded data.
1397        let mut crafted = Vec::new();
1398        srcmap_codec::vlq_encode_unsigned(&mut crafted, 18); // unknown tag
1399        srcmap_codec::vlq_encode_unsigned(&mut crafted, 42); // dummy VLQ 1
1400        srcmap_codec::vlq_encode_unsigned(&mut crafted, 7); // dummy VLQ 2
1401        crafted.push(b',');
1402        crafted.extend_from_slice(encoded.as_bytes());
1403
1404        let crafted_str = std::str::from_utf8(&crafted).unwrap();
1405        let decoded = decode_scopes(crafted_str, &names, 1).unwrap();
1406
1407        assert_eq!(decoded.scopes.len(), 1);
1408        assert!(decoded.scopes[0].is_some());
1409        let scope = decoded.scopes[0].as_ref().unwrap();
1410        assert_eq!(scope.start, Position { line: 0, column: 0 });
1411        assert_eq!(scope.end, Position { line: 5, column: 0 });
1412    }
1413
1414    #[test]
1415    fn first_source_none_exercises_empty_path() {
1416        // First source has no scopes (None), second has scopes.
1417        // This exercises encode.rs line 89 (first_item && scope.is_none())
1418        // and decode.rs lines 124, 129 (empty item + skip_comma)
1419        let info = ScopeInfo {
1420            scopes: vec![
1421                None,
1422                None,
1423                Some(OriginalScope {
1424                    start: Position { line: 0, column: 0 },
1425                    end: Position { line: 5, column: 0 },
1426                    name: None,
1427                    kind: None,
1428                    is_stack_frame: false,
1429                    variables: vec![],
1430                    children: vec![],
1431                }),
1432            ],
1433            ranges: vec![],
1434        };
1435
1436        let mut names = vec![];
1437        let encoded = encode_scopes(&info, &mut names);
1438        let decoded = decode_scopes(&encoded, &names, 3).unwrap();
1439
1440        assert_eq!(decoded.scopes.len(), 3);
1441        assert!(decoded.scopes[0].is_none());
1442        assert!(decoded.scopes[1].is_none());
1443        assert!(decoded.scopes[2].is_some());
1444    }
1445
1446    #[test]
1447    fn scope_end_same_line_as_child_end() {
1448        // Parent scope ends on the same line where child scope ended.
1449        // This exercises the column-relative path in TAG_ORIGINAL_SCOPE_END
1450        // (decode.rs lines 183, 186, 188 where line_delta = 0)
1451        let info = ScopeInfo {
1452            scopes: vec![Some(OriginalScope {
1453                start: Position { line: 0, column: 0 },
1454                end: Position { line: 10, column: 20 },
1455                name: None,
1456                kind: None,
1457                is_stack_frame: false,
1458                variables: vec![],
1459                children: vec![OriginalScope {
1460                    start: Position { line: 5, column: 0 },
1461                    end: Position { line: 10, column: 10 },
1462                    name: None,
1463                    kind: None,
1464                    is_stack_frame: false,
1465                    variables: vec![],
1466                    children: vec![],
1467                }],
1468            })],
1469            ranges: vec![],
1470        };
1471
1472        let mut names = vec![];
1473        let encoded = encode_scopes(&info, &mut names);
1474        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1475
1476        let scope = decoded.scopes[0].as_ref().unwrap();
1477        assert_eq!(scope.end, Position { line: 10, column: 20 });
1478        assert_eq!(scope.children[0].end, Position { line: 10, column: 10 });
1479    }
1480
1481    #[test]
1482    fn generated_range_has_line_flag() {
1483        // Range starting on a non-zero line (exercises GR_FLAG_HAS_LINE)
1484        // Covers decode.rs lines 232, 233 (has_line flag, read line delta)
1485        // and encode.rs line_delta != 0 path
1486        let info = ScopeInfo {
1487            scopes: vec![],
1488            ranges: vec![
1489                GeneratedRange {
1490                    start: Position { line: 0, column: 5 },
1491                    end: Position { line: 0, column: 50 },
1492                    is_stack_frame: false,
1493                    is_hidden: false,
1494                    definition: None,
1495                    call_site: None,
1496                    bindings: vec![],
1497                    children: vec![],
1498                },
1499                GeneratedRange {
1500                    start: Position { line: 3, column: 10 },
1501                    end: Position { line: 8, column: 20 },
1502                    is_stack_frame: false,
1503                    is_hidden: false,
1504                    definition: None,
1505                    call_site: None,
1506                    bindings: vec![],
1507                    children: vec![],
1508                },
1509            ],
1510        };
1511
1512        let mut names = vec![];
1513        let encoded = encode_scopes(&info, &mut names);
1514        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
1515
1516        assert_eq!(decoded.ranges.len(), 2);
1517        assert_eq!(decoded.ranges[0].start, Position { line: 0, column: 5 });
1518        assert_eq!(decoded.ranges[0].end, Position { line: 0, column: 50 });
1519        assert_eq!(decoded.ranges[1].start, Position { line: 3, column: 10 });
1520        assert_eq!(decoded.ranges[1].end, Position { line: 8, column: 20 });
1521    }
1522
1523    #[test]
1524    fn scope_variables_decode_path() {
1525        // Scope with variables on a child to exercise TAG_ORIGINAL_SCOPE_VARIABLES
1526        // decode path (decode.rs lines 221, 223, 225)
1527        let info = ScopeInfo {
1528            scopes: vec![Some(OriginalScope {
1529                start: Position { line: 0, column: 0 },
1530                end: Position { line: 20, column: 0 },
1531                name: None,
1532                kind: None,
1533                is_stack_frame: false,
1534                variables: vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()],
1535                children: vec![OriginalScope {
1536                    start: Position { line: 2, column: 0 },
1537                    end: Position { line: 18, column: 0 },
1538                    name: None,
1539                    kind: None,
1540                    is_stack_frame: false,
1541                    variables: vec!["delta".to_string(), "epsilon".to_string()],
1542                    children: vec![],
1543                }],
1544            })],
1545            ranges: vec![],
1546        };
1547
1548        let mut names = vec![];
1549        let encoded = encode_scopes(&info, &mut names);
1550        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1551
1552        let scope = decoded.scopes[0].as_ref().unwrap();
1553        assert_eq!(scope.variables, vec!["alpha", "beta", "gamma"]);
1554        assert_eq!(scope.children[0].variables, vec!["delta", "epsilon"]);
1555    }
1556
1557    #[test]
1558    fn sub_range_first_expression_none() {
1559        // Sub-ranges where the first expression is None (Unavailable at range start).
1560        // This exercises encode.rs line 267 (first sub expression is None → emit 0 in G)
1561        let info = ScopeInfo {
1562            scopes: vec![Some(OriginalScope {
1563                start: Position { line: 0, column: 0 },
1564                end: Position { line: 20, column: 0 },
1565                name: None,
1566                kind: None,
1567                is_stack_frame: false,
1568                variables: vec!["v".to_string()],
1569                children: vec![],
1570            })],
1571            ranges: vec![GeneratedRange {
1572                start: Position { line: 0, column: 0 },
1573                end: Position { line: 20, column: 0 },
1574                is_stack_frame: false,
1575                is_hidden: false,
1576                definition: Some(0),
1577                call_site: None,
1578                bindings: vec![Binding::SubRanges(vec![
1579                    SubRangeBinding { expression: None, from: Position { line: 0, column: 0 } },
1580                    SubRangeBinding {
1581                        expression: Some("_v".to_string()),
1582                        from: Position { line: 8, column: 0 },
1583                    },
1584                ])],
1585                children: vec![],
1586            }],
1587        };
1588
1589        let mut names = vec![];
1590        let encoded = encode_scopes(&info, &mut names);
1591        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1592
1593        match &decoded.ranges[0].bindings[0] {
1594            Binding::SubRanges(subs) => {
1595                assert_eq!(subs.len(), 2);
1596                assert_eq!(subs[0].expression, None);
1597                assert_eq!(subs[0].from, Position { line: 0, column: 0 });
1598                assert_eq!(subs[1].expression.as_deref(), Some("_v"));
1599                assert_eq!(subs[1].from, Position { line: 8, column: 0 });
1600            }
1601            other => panic!("expected SubRanges, got {other:?}"),
1602        }
1603    }
1604
1605    #[test]
1606    fn comprehensive_roundtrip() {
1607        // Full roundtrip combining scopes with name+kind, nested ranges with
1608        // call sites, sub-range bindings, unavailable bindings, and hidden ranges.
1609        let info = ScopeInfo {
1610            scopes: vec![
1611                Some(OriginalScope {
1612                    start: Position { line: 0, column: 0 },
1613                    end: Position { line: 50, column: 0 },
1614                    name: None,
1615                    kind: Some("module".to_string()),
1616                    is_stack_frame: false,
1617                    variables: vec!["exports".to_string()],
1618                    children: vec![
1619                        OriginalScope {
1620                            start: Position { line: 2, column: 0 },
1621                            end: Position { line: 20, column: 1 },
1622                            name: Some("add".to_string()),
1623                            kind: Some("function".to_string()),
1624                            is_stack_frame: true,
1625                            variables: vec!["a".to_string(), "b".to_string()],
1626                            children: vec![],
1627                        },
1628                        OriginalScope {
1629                            start: Position { line: 22, column: 0 },
1630                            end: Position { line: 40, column: 1 },
1631                            name: Some("multiply".to_string()),
1632                            kind: Some("function".to_string()),
1633                            is_stack_frame: true,
1634                            variables: vec!["x".to_string(), "y".to_string()],
1635                            children: vec![],
1636                        },
1637                    ],
1638                }),
1639                None, // second source: no scopes
1640            ],
1641            ranges: vec![GeneratedRange {
1642                start: Position { line: 0, column: 0 },
1643                end: Position { line: 25, column: 0 },
1644                is_stack_frame: false,
1645                is_hidden: false,
1646                definition: Some(0),
1647                call_site: None,
1648                bindings: vec![Binding::Expression("module.exports".to_string())],
1649                children: vec![
1650                    GeneratedRange {
1651                        start: Position { line: 1, column: 0 },
1652                        end: Position { line: 10, column: 0 },
1653                        is_stack_frame: true,
1654                        is_hidden: false,
1655                        definition: Some(1),
1656                        call_site: Some(CallSite { source_index: 0, line: 45, column: 2 }),
1657                        bindings: vec![Binding::Expression("_a".to_string()), Binding::Unavailable],
1658                        children: vec![],
1659                    },
1660                    GeneratedRange {
1661                        start: Position { line: 12, column: 0 },
1662                        end: Position { line: 20, column: 0 },
1663                        is_stack_frame: true,
1664                        is_hidden: true,
1665                        definition: Some(2),
1666                        call_site: Some(CallSite { source_index: 0, line: 46, column: 0 }),
1667                        bindings: vec![
1668                            Binding::SubRanges(vec![
1669                                SubRangeBinding {
1670                                    expression: Some("p1".to_string()),
1671                                    from: Position { line: 12, column: 0 },
1672                                },
1673                                SubRangeBinding {
1674                                    expression: Some("p2".to_string()),
1675                                    from: Position { line: 16, column: 0 },
1676                                },
1677                            ]),
1678                            Binding::Expression("_y".to_string()),
1679                        ],
1680                        children: vec![],
1681                    },
1682                ],
1683            }],
1684        };
1685
1686        let mut names = vec![];
1687        let encoded = encode_scopes(&info, &mut names);
1688        let decoded = decode_scopes(&encoded, &names, 2).unwrap();
1689
1690        // Verify scopes
1691        assert_eq!(decoded.scopes.len(), 2);
1692        assert!(decoded.scopes[1].is_none());
1693        let root = decoded.scopes[0].as_ref().unwrap();
1694        assert_eq!(root.kind.as_deref(), Some("module"));
1695        assert_eq!(root.children.len(), 2);
1696        assert_eq!(root.children[0].name.as_deref(), Some("add"));
1697        assert_eq!(root.children[1].name.as_deref(), Some("multiply"));
1698
1699        // Verify ranges
1700        assert_eq!(decoded.ranges.len(), 1);
1701        let outer = &decoded.ranges[0];
1702        assert_eq!(outer.children.len(), 2);
1703
1704        // First child: inlined add
1705        let add_range = &outer.children[0];
1706        assert!(add_range.is_stack_frame);
1707        assert!(!add_range.is_hidden);
1708        assert_eq!(add_range.definition, Some(1));
1709        assert_eq!(add_range.call_site, Some(CallSite { source_index: 0, line: 45, column: 2 }));
1710        assert_eq!(add_range.bindings[0], Binding::Expression("_a".to_string()));
1711        assert_eq!(add_range.bindings[1], Binding::Unavailable);
1712
1713        // Second child: inlined multiply (hidden)
1714        let mul_range = &outer.children[1];
1715        assert!(mul_range.is_stack_frame);
1716        assert!(mul_range.is_hidden);
1717        assert_eq!(mul_range.definition, Some(2));
1718        match &mul_range.bindings[0] {
1719            Binding::SubRanges(subs) => {
1720                assert_eq!(subs.len(), 2);
1721                assert_eq!(subs[0].expression.as_deref(), Some("p1"));
1722                assert_eq!(subs[1].expression.as_deref(), Some("p2"));
1723            }
1724            other => panic!("expected SubRanges, got {other:?}"),
1725        }
1726        assert_eq!(mul_range.bindings[1], Binding::Expression("_y".to_string()));
1727    }
1728
1729    #[test]
1730    fn range_end_column_only_1vlq() {
1731        // Range where end is on the same line as a previous position, resulting
1732        // in a 1-VLQ range end (column only). The parent range ending on line 5
1733        // after the child also ended on line 5.
1734        let info = ScopeInfo {
1735            scopes: vec![],
1736            ranges: vec![GeneratedRange {
1737                start: Position { line: 0, column: 0 },
1738                end: Position { line: 5, column: 50 },
1739                is_stack_frame: false,
1740                is_hidden: false,
1741                definition: None,
1742                call_site: None,
1743                bindings: vec![],
1744                children: vec![GeneratedRange {
1745                    start: Position { line: 2, column: 0 },
1746                    end: Position { line: 5, column: 30 },
1747                    is_stack_frame: false,
1748                    is_hidden: false,
1749                    definition: None,
1750                    call_site: None,
1751                    bindings: vec![],
1752                    children: vec![],
1753                }],
1754            }],
1755        };
1756
1757        let mut names = vec![];
1758        let encoded = encode_scopes(&info, &mut names);
1759        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
1760
1761        let outer = &decoded.ranges[0];
1762        assert_eq!(outer.end, Position { line: 5, column: 50 });
1763        let inner = &outer.children[0];
1764        assert_eq!(inner.end, Position { line: 5, column: 30 });
1765    }
1766
1767    #[test]
1768    fn all_sources_none_with_ranges() {
1769        // All sources have None scopes, but ranges exist.
1770        // Exercises the transition to in_generated_ranges when scopes are all None.
1771        let info = ScopeInfo {
1772            scopes: vec![None, None, None],
1773            ranges: vec![GeneratedRange {
1774                start: Position { line: 0, column: 0 },
1775                end: Position { line: 10, column: 0 },
1776                is_stack_frame: false,
1777                is_hidden: false,
1778                definition: None,
1779                call_site: None,
1780                bindings: vec![],
1781                children: vec![],
1782            }],
1783        };
1784
1785        let mut names = vec![];
1786        let encoded = encode_scopes(&info, &mut names);
1787        let decoded = decode_scopes(&encoded, &names, 3).unwrap();
1788
1789        assert_eq!(decoded.scopes.len(), 3);
1790        assert!(decoded.scopes.iter().all(|s| s.is_none()));
1791        assert_eq!(decoded.ranges.len(), 1);
1792    }
1793
1794    #[test]
1795    fn scope_name_only_no_kind() {
1796        // Scope with name but no kind (exercises OS_FLAG_HAS_NAME without OS_FLAG_HAS_KIND)
1797        let info = ScopeInfo {
1798            scopes: vec![Some(OriginalScope {
1799                start: Position { line: 0, column: 0 },
1800                end: Position { line: 5, column: 0 },
1801                name: Some("myVar".to_string()),
1802                kind: None,
1803                is_stack_frame: false,
1804                variables: vec![],
1805                children: vec![],
1806            })],
1807            ranges: vec![],
1808        };
1809
1810        let mut names = vec![];
1811        let encoded = encode_scopes(&info, &mut names);
1812        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1813
1814        let scope = decoded.scopes[0].as_ref().unwrap();
1815        assert_eq!(scope.name.as_deref(), Some("myVar"));
1816        assert_eq!(scope.kind, None);
1817    }
1818
1819    #[test]
1820    fn generated_range_with_definition_on_nonzero_line() {
1821        // Exercises GR_FLAG_HAS_LINE | GR_FLAG_HAS_DEFINITION together
1822        // Covers decode.rs lines 238, 241, 247, 255
1823        let info = ScopeInfo {
1824            scopes: vec![Some(OriginalScope {
1825                start: Position { line: 0, column: 0 },
1826                end: Position { line: 50, column: 0 },
1827                name: None,
1828                kind: None,
1829                is_stack_frame: false,
1830                variables: vec![],
1831                children: vec![],
1832            })],
1833            ranges: vec![GeneratedRange {
1834                start: Position { line: 10, column: 5 },
1835                end: Position { line: 40, column: 0 },
1836                is_stack_frame: true,
1837                is_hidden: false,
1838                definition: Some(0),
1839                call_site: None,
1840                bindings: vec![],
1841                children: vec![],
1842            }],
1843        };
1844
1845        let mut names = vec![];
1846        let encoded = encode_scopes(&info, &mut names);
1847        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1848
1849        let range = &decoded.ranges[0];
1850        assert_eq!(range.start, Position { line: 10, column: 5 });
1851        assert_eq!(range.end, Position { line: 40, column: 0 });
1852        assert!(range.is_stack_frame);
1853        assert_eq!(range.definition, Some(0));
1854    }
1855
1856    // ── Error path tests ───────────────────────────────────────────
1857
1858    #[test]
1859    fn decode_unmatched_scope_end() {
1860        // Craft a raw string with TAG_ORIGINAL_SCOPE_END (0x2 = 'C') without preceding start
1861        // TAG=2 (C), line_delta=0 (A), col=0 (A)
1862        let raw = "CAA";
1863        let names: Vec<String> = vec![];
1864        let err = decode_scopes(raw, &names, 1).unwrap_err();
1865        assert!(matches!(err, ScopesError::UnmatchedScopeEnd));
1866    }
1867
1868    #[test]
1869    fn decode_unclosed_scope() {
1870        // TAG_ORIGINAL_SCOPE_START (0x1 = 'B'), flags=0 (A), line=0 (A), col=0 (A)
1871        // No matching end tag
1872        let raw = "BAAA";
1873        let names: Vec<String> = vec![];
1874        let err = decode_scopes(raw, &names, 1).unwrap_err();
1875        assert!(matches!(err, ScopesError::UnclosedScope));
1876    }
1877
1878    #[test]
1879    fn decode_unmatched_range_end() {
1880        // First fill 1 source scope (empty) with comma separator
1881        // Then TAG_GENERATED_RANGE_END (0x5 = 'F'), col=0 (A)
1882        // Empty scope for source 0, then comma, then range end tag
1883        let raw = ",FA";
1884        let names: Vec<String> = vec![];
1885        let err = decode_scopes(raw, &names, 1).unwrap_err();
1886        assert!(matches!(err, ScopesError::UnmatchedRangeEnd));
1887    }
1888
1889    #[test]
1890    fn decode_unclosed_range() {
1891        // Fill 1 source (empty), then start a range without closing it
1892        // TAG_GENERATED_RANGE_START (0x4 = 'E'), flags=0 (A), col=0 (A)
1893        let raw = ",EAAA";
1894        let names: Vec<String> = vec![];
1895        let err = decode_scopes(raw, &names, 1).unwrap_err();
1896        assert!(matches!(err, ScopesError::UnclosedRange));
1897    }
1898}