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 {
309                    line: 10,
310                    column: 0,
311                },
312                name: None,
313                kind: Some("global".to_string()),
314                is_stack_frame: false,
315                variables: vec!["x".to_string(), "y".to_string()],
316                children: vec![],
317            })],
318            ranges: vec![],
319        };
320
321        let mut names = vec![];
322        let encoded = encode_scopes(&info, &mut names);
323        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
324
325        assert_eq!(decoded.scopes.len(), 1);
326        let scope = decoded.scopes[0].as_ref().unwrap();
327        assert_eq!(scope.start, Position { line: 0, column: 0 });
328        assert_eq!(
329            scope.end,
330            Position {
331                line: 10,
332                column: 0
333            }
334        );
335        assert_eq!(scope.kind.as_deref(), Some("global"));
336        assert_eq!(scope.name, None);
337        assert!(!scope.is_stack_frame);
338        assert_eq!(scope.variables, vec!["x", "y"]);
339    }
340
341    #[test]
342    fn nested_scopes_roundtrip() {
343        let info = ScopeInfo {
344            scopes: vec![Some(OriginalScope {
345                start: Position { line: 0, column: 0 },
346                end: Position {
347                    line: 10,
348                    column: 1,
349                },
350                name: None,
351                kind: Some("global".to_string()),
352                is_stack_frame: false,
353                variables: vec!["z".to_string()],
354                children: vec![OriginalScope {
355                    start: Position { line: 1, column: 0 },
356                    end: Position { line: 5, column: 1 },
357                    name: Some("hello".to_string()),
358                    kind: Some("function".to_string()),
359                    is_stack_frame: true,
360                    variables: vec!["msg".to_string(), "result".to_string()],
361                    children: vec![],
362                }],
363            })],
364            ranges: vec![],
365        };
366
367        let mut names = vec![];
368        let encoded = encode_scopes(&info, &mut names);
369        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
370
371        let scope = decoded.scopes[0].as_ref().unwrap();
372        assert_eq!(scope.children.len(), 1);
373        let child = &scope.children[0];
374        assert_eq!(child.start, Position { line: 1, column: 0 });
375        assert_eq!(child.end, Position { line: 5, column: 1 });
376        assert_eq!(child.name.as_deref(), Some("hello"));
377        assert_eq!(child.kind.as_deref(), Some("function"));
378        assert!(child.is_stack_frame);
379        assert_eq!(child.variables, vec!["msg", "result"]);
380    }
381
382    #[test]
383    fn multiple_sources_with_gaps() {
384        let info = ScopeInfo {
385            scopes: vec![
386                Some(OriginalScope {
387                    start: Position { line: 0, column: 0 },
388                    end: Position { line: 5, column: 0 },
389                    name: None,
390                    kind: None,
391                    is_stack_frame: false,
392                    variables: vec![],
393                    children: vec![],
394                }),
395                None, // second source has no scopes
396                Some(OriginalScope {
397                    start: Position { line: 0, column: 0 },
398                    end: Position { line: 3, column: 0 },
399                    name: None,
400                    kind: None,
401                    is_stack_frame: false,
402                    variables: vec![],
403                    children: vec![],
404                }),
405            ],
406            ranges: vec![],
407        };
408
409        let mut names = vec![];
410        let encoded = encode_scopes(&info, &mut names);
411        let decoded = decode_scopes(&encoded, &names, 3).unwrap();
412
413        assert_eq!(decoded.scopes.len(), 3);
414        assert!(decoded.scopes[0].is_some());
415        assert!(decoded.scopes[1].is_none());
416        assert!(decoded.scopes[2].is_some());
417    }
418
419    #[test]
420    fn generated_ranges_roundtrip() {
421        let info = ScopeInfo {
422            scopes: vec![Some(OriginalScope {
423                start: Position { line: 0, column: 0 },
424                end: Position {
425                    line: 10,
426                    column: 0,
427                },
428                name: None,
429                kind: Some("global".to_string()),
430                is_stack_frame: false,
431                variables: vec!["x".to_string()],
432                children: vec![],
433            })],
434            ranges: vec![GeneratedRange {
435                start: Position { line: 0, column: 0 },
436                end: Position {
437                    line: 10,
438                    column: 0,
439                },
440                is_stack_frame: false,
441                is_hidden: false,
442                definition: Some(0),
443                call_site: None,
444                bindings: vec![Binding::Expression("_x".to_string())],
445                children: vec![],
446            }],
447        };
448
449        let mut names = vec![];
450        let encoded = encode_scopes(&info, &mut names);
451        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
452
453        assert_eq!(decoded.ranges.len(), 1);
454        let range = &decoded.ranges[0];
455        assert_eq!(range.start, Position { line: 0, column: 0 });
456        assert_eq!(
457            range.end,
458            Position {
459                line: 10,
460                column: 0
461            }
462        );
463        assert_eq!(range.definition, Some(0));
464        assert_eq!(range.bindings, vec![Binding::Expression("_x".to_string())]);
465    }
466
467    #[test]
468    fn nested_ranges_with_inlining() {
469        let info = ScopeInfo {
470            scopes: vec![Some(OriginalScope {
471                start: Position { line: 0, column: 0 },
472                end: Position {
473                    line: 10,
474                    column: 0,
475                },
476                name: None,
477                kind: Some("global".to_string()),
478                is_stack_frame: false,
479                variables: vec!["x".to_string()],
480                children: vec![OriginalScope {
481                    start: Position { line: 1, column: 0 },
482                    end: Position { line: 5, column: 1 },
483                    name: Some("fn1".to_string()),
484                    kind: Some("function".to_string()),
485                    is_stack_frame: true,
486                    variables: vec!["a".to_string()],
487                    children: vec![],
488                }],
489            })],
490            ranges: vec![GeneratedRange {
491                start: Position { line: 0, column: 0 },
492                end: Position {
493                    line: 10,
494                    column: 0,
495                },
496                is_stack_frame: false,
497                is_hidden: false,
498                definition: Some(0),
499                call_site: None,
500                bindings: vec![Binding::Expression("_x".to_string())],
501                children: vec![GeneratedRange {
502                    start: Position { line: 6, column: 0 },
503                    end: Position {
504                        line: 8,
505                        column: 20,
506                    },
507                    is_stack_frame: true,
508                    is_hidden: false,
509                    definition: Some(1),
510                    call_site: Some(CallSite {
511                        source_index: 0,
512                        line: 7,
513                        column: 0,
514                    }),
515                    bindings: vec![Binding::Expression("\"hello\"".to_string())],
516                    children: vec![],
517                }],
518            }],
519        };
520
521        let mut names = vec![];
522        let encoded = encode_scopes(&info, &mut names);
523        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
524
525        assert_eq!(decoded.ranges.len(), 1);
526        let outer = &decoded.ranges[0];
527        assert_eq!(outer.children.len(), 1);
528        let inner = &outer.children[0];
529        assert!(inner.is_stack_frame);
530        assert_eq!(inner.definition, Some(1));
531        assert_eq!(
532            inner.call_site,
533            Some(CallSite {
534                source_index: 0,
535                line: 7,
536                column: 0,
537            })
538        );
539        assert_eq!(
540            inner.bindings,
541            vec![Binding::Expression("\"hello\"".to_string())]
542        );
543    }
544
545    #[test]
546    fn unavailable_bindings() {
547        let info = ScopeInfo {
548            scopes: vec![Some(OriginalScope {
549                start: Position { line: 0, column: 0 },
550                end: Position { line: 5, column: 0 },
551                name: None,
552                kind: None,
553                is_stack_frame: false,
554                variables: vec!["a".to_string(), "b".to_string()],
555                children: vec![],
556            })],
557            ranges: vec![GeneratedRange {
558                start: Position { line: 0, column: 0 },
559                end: Position { line: 5, column: 0 },
560                is_stack_frame: false,
561                is_hidden: false,
562                definition: Some(0),
563                call_site: None,
564                bindings: vec![Binding::Expression("_a".to_string()), Binding::Unavailable],
565                children: vec![],
566            }],
567        };
568
569        let mut names = vec![];
570        let encoded = encode_scopes(&info, &mut names);
571        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
572
573        assert_eq!(
574            decoded.ranges[0].bindings,
575            vec![Binding::Expression("_a".to_string()), Binding::Unavailable,]
576        );
577    }
578
579    #[test]
580    fn sub_range_bindings_roundtrip() {
581        let info = ScopeInfo {
582            scopes: vec![Some(OriginalScope {
583                start: Position { line: 0, column: 0 },
584                end: Position {
585                    line: 20,
586                    column: 0,
587                },
588                name: None,
589                kind: None,
590                is_stack_frame: false,
591                variables: vec!["x".to_string(), "y".to_string()],
592                children: vec![],
593            })],
594            ranges: vec![GeneratedRange {
595                start: Position { line: 0, column: 0 },
596                end: Position {
597                    line: 20,
598                    column: 0,
599                },
600                is_stack_frame: false,
601                is_hidden: false,
602                definition: Some(0),
603                call_site: None,
604                bindings: vec![
605                    Binding::SubRanges(vec![
606                        SubRangeBinding {
607                            expression: Some("a".to_string()),
608                            from: Position { line: 0, column: 0 },
609                        },
610                        SubRangeBinding {
611                            expression: Some("b".to_string()),
612                            from: Position { line: 5, column: 0 },
613                        },
614                        SubRangeBinding {
615                            expression: None,
616                            from: Position {
617                                line: 10,
618                                column: 0,
619                            },
620                        },
621                    ]),
622                    Binding::Expression("_y".to_string()),
623                ],
624                children: vec![],
625            }],
626        };
627
628        let mut names = vec![];
629        let encoded = encode_scopes(&info, &mut names);
630        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
631
632        let bindings = &decoded.ranges[0].bindings;
633        assert_eq!(bindings.len(), 2);
634
635        match &bindings[0] {
636            Binding::SubRanges(subs) => {
637                assert_eq!(subs.len(), 3);
638                assert_eq!(subs[0].expression.as_deref(), Some("a"));
639                assert_eq!(subs[0].from, Position { line: 0, column: 0 });
640                assert_eq!(subs[1].expression.as_deref(), Some("b"));
641                assert_eq!(subs[1].from, Position { line: 5, column: 0 });
642                assert_eq!(subs[2].expression, None);
643                assert_eq!(
644                    subs[2].from,
645                    Position {
646                        line: 10,
647                        column: 0,
648                    }
649                );
650            }
651            other => panic!("expected SubRanges, got {other:?}"),
652        }
653        assert_eq!(bindings[1], Binding::Expression("_y".to_string()));
654    }
655
656    #[test]
657    fn hidden_range() {
658        let info = ScopeInfo {
659            scopes: vec![Some(OriginalScope {
660                start: Position { line: 0, column: 0 },
661                end: Position { line: 5, column: 0 },
662                name: None,
663                kind: None,
664                is_stack_frame: false,
665                variables: vec![],
666                children: vec![],
667            })],
668            ranges: vec![GeneratedRange {
669                start: Position { line: 0, column: 0 },
670                end: Position { line: 5, column: 0 },
671                is_stack_frame: true,
672                is_hidden: true,
673                definition: Some(0),
674                call_site: None,
675                bindings: vec![],
676                children: vec![],
677            }],
678        };
679
680        let mut names = vec![];
681        let encoded = encode_scopes(&info, &mut names);
682        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
683
684        assert!(decoded.ranges[0].is_stack_frame);
685        assert!(decoded.ranges[0].is_hidden);
686    }
687
688    #[test]
689    fn definition_resolution() {
690        let info = ScopeInfo {
691            scopes: vec![Some(OriginalScope {
692                start: Position { line: 0, column: 0 },
693                end: Position {
694                    line: 10,
695                    column: 0,
696                },
697                name: None,
698                kind: Some("global".to_string()),
699                is_stack_frame: false,
700                variables: vec![],
701                children: vec![
702                    OriginalScope {
703                        start: Position { line: 1, column: 0 },
704                        end: Position { line: 4, column: 1 },
705                        name: Some("foo".to_string()),
706                        kind: Some("function".to_string()),
707                        is_stack_frame: true,
708                        variables: vec![],
709                        children: vec![],
710                    },
711                    OriginalScope {
712                        start: Position { line: 5, column: 0 },
713                        end: Position { line: 9, column: 1 },
714                        name: Some("bar".to_string()),
715                        kind: Some("function".to_string()),
716                        is_stack_frame: true,
717                        variables: vec![],
718                        children: vec![],
719                    },
720                ],
721            })],
722            ranges: vec![],
723        };
724
725        // Definition 0 = global scope
726        let scope0 = info.original_scope_for_definition(0).unwrap();
727        assert_eq!(scope0.kind.as_deref(), Some("global"));
728
729        // Definition 1 = foo
730        let scope1 = info.original_scope_for_definition(1).unwrap();
731        assert_eq!(scope1.name.as_deref(), Some("foo"));
732
733        // Definition 2 = bar
734        let scope2 = info.original_scope_for_definition(2).unwrap();
735        assert_eq!(scope2.name.as_deref(), Some("bar"));
736
737        // Definition 3 = out of bounds
738        assert!(info.original_scope_for_definition(3).is_none());
739    }
740
741    #[test]
742    fn scopes_only_no_ranges() {
743        let info = ScopeInfo {
744            scopes: vec![Some(OriginalScope {
745                start: Position { line: 0, column: 0 },
746                end: Position { line: 5, column: 0 },
747                name: None,
748                kind: None,
749                is_stack_frame: false,
750                variables: vec![],
751                children: vec![],
752            })],
753            ranges: vec![],
754        };
755
756        let mut names = vec![];
757        let encoded = encode_scopes(&info, &mut names);
758        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
759
760        assert_eq!(decoded.scopes.len(), 1);
761        assert!(decoded.scopes[0].is_some());
762        assert!(decoded.ranges.is_empty());
763    }
764
765    #[test]
766    fn ranges_only_no_scopes() {
767        let info = ScopeInfo {
768            scopes: vec![None],
769            ranges: vec![GeneratedRange {
770                start: Position { line: 0, column: 0 },
771                end: Position { line: 5, column: 0 },
772                is_stack_frame: false,
773                is_hidden: false,
774                definition: None,
775                call_site: None,
776                bindings: vec![],
777                children: vec![],
778            }],
779        };
780
781        let mut names = vec![];
782        let encoded = encode_scopes(&info, &mut names);
783        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
784
785        assert_eq!(decoded.scopes.len(), 1);
786        assert!(decoded.scopes[0].is_none());
787        assert_eq!(decoded.ranges.len(), 1);
788    }
789
790    #[test]
791    fn range_no_definition() {
792        let info = ScopeInfo {
793            scopes: vec![],
794            ranges: vec![GeneratedRange {
795                start: Position { line: 0, column: 0 },
796                end: Position { line: 5, column: 0 },
797                is_stack_frame: false,
798                is_hidden: false,
799                definition: None,
800                call_site: None,
801                bindings: vec![],
802                children: vec![],
803            }],
804        };
805
806        let mut names = vec![];
807        let encoded = encode_scopes(&info, &mut names);
808        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
809
810        assert_eq!(decoded.ranges.len(), 1);
811        assert_eq!(decoded.ranges[0].definition, None);
812    }
813
814    #[test]
815    fn scopes_error_display() {
816        let err = ScopesError::UnmatchedScopeEnd;
817        assert_eq!(err.to_string(), "scope end without matching start");
818
819        let err = ScopesError::UnclosedScope;
820        assert_eq!(err.to_string(), "scope opened but never closed");
821
822        let err = ScopesError::UnmatchedRangeEnd;
823        assert_eq!(err.to_string(), "range end without matching start");
824
825        let err = ScopesError::UnclosedRange;
826        assert_eq!(err.to_string(), "range opened but never closed");
827
828        let err = ScopesError::InvalidNameIndex(42);
829        assert_eq!(err.to_string(), "invalid name index: 42");
830
831        let vlq_err = srcmap_codec::DecodeError::UnexpectedEof { offset: 5 };
832        let err = ScopesError::Vlq(vlq_err);
833        assert!(err.to_string().contains("VLQ decode error"));
834    }
835
836    #[test]
837    fn scopes_error_from_decode_error() {
838        let vlq_err = srcmap_codec::DecodeError::UnexpectedEof { offset: 0 };
839        let err: ScopesError = vlq_err.into();
840        assert!(matches!(err, ScopesError::Vlq(_)));
841    }
842
843    #[test]
844    fn invalid_name_index_error() {
845        // Encode scopes that reference name index 0, but pass empty names array to decode
846        let info = ScopeInfo {
847            scopes: vec![Some(OriginalScope {
848                start: Position { line: 0, column: 0 },
849                end: Position { line: 5, column: 0 },
850                name: Some("test".to_string()),
851                kind: None,
852                is_stack_frame: false,
853                variables: vec![],
854                children: vec![],
855            })],
856            ranges: vec![],
857        };
858
859        let mut names = vec![];
860        let encoded = encode_scopes(&info, &mut names);
861        // Now decode with empty names - should fail with InvalidNameIndex
862        let err = decode_scopes(&encoded, &[], 1).unwrap_err();
863        assert!(matches!(err, ScopesError::InvalidNameIndex(_)));
864    }
865
866    #[test]
867    fn invalid_binding_index_error() {
868        // Create a range with a binding expression that requires name index 0
869        let info = ScopeInfo {
870            scopes: vec![Some(OriginalScope {
871                start: Position { line: 0, column: 0 },
872                end: Position { line: 5, column: 0 },
873                name: None,
874                kind: None,
875                is_stack_frame: false,
876                variables: vec!["x".to_string()],
877                children: vec![],
878            })],
879            ranges: vec![GeneratedRange {
880                start: Position { line: 0, column: 0 },
881                end: Position { line: 5, column: 0 },
882                is_stack_frame: false,
883                is_hidden: false,
884                definition: Some(0),
885                call_site: None,
886                bindings: vec![Binding::Expression("_x".to_string())],
887                children: vec![],
888            }],
889        };
890
891        let mut names = vec![];
892        let encoded = encode_scopes(&info, &mut names);
893        // Decode with truncated names (remove the binding expression name)
894        let short_names: Vec<String> = names.iter().take(1).cloned().collect();
895        let err = decode_scopes(&encoded, &short_names, 1).unwrap_err();
896        assert!(matches!(err, ScopesError::InvalidNameIndex(_)));
897    }
898
899    #[test]
900    fn scope_same_line_end() {
901        // Scope that starts and ends on the same line (column relative)
902        let info = ScopeInfo {
903            scopes: vec![Some(OriginalScope {
904                start: Position {
905                    line: 5,
906                    column: 10,
907                },
908                end: Position {
909                    line: 5,
910                    column: 30,
911                },
912                name: None,
913                kind: None,
914                is_stack_frame: false,
915                variables: vec![],
916                children: vec![],
917            })],
918            ranges: vec![],
919        };
920
921        let mut names = vec![];
922        let encoded = encode_scopes(&info, &mut names);
923        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
924
925        let scope = decoded.scopes[0].as_ref().unwrap();
926        assert_eq!(
927            scope.start,
928            Position {
929                line: 5,
930                column: 10
931            }
932        );
933        assert_eq!(
934            scope.end,
935            Position {
936                line: 5,
937                column: 30
938            }
939        );
940    }
941
942    #[test]
943    fn range_same_line() {
944        // Range that starts and ends on the same line
945        let info = ScopeInfo {
946            scopes: vec![Some(OriginalScope {
947                start: Position { line: 0, column: 0 },
948                end: Position {
949                    line: 10,
950                    column: 0,
951                },
952                name: None,
953                kind: None,
954                is_stack_frame: false,
955                variables: vec![],
956                children: vec![],
957            })],
958            ranges: vec![GeneratedRange {
959                start: Position { line: 3, column: 5 },
960                end: Position {
961                    line: 3,
962                    column: 25,
963                },
964                is_stack_frame: false,
965                is_hidden: false,
966                definition: Some(0),
967                call_site: None,
968                bindings: vec![],
969                children: vec![],
970            }],
971        };
972
973        let mut names = vec![];
974        let encoded = encode_scopes(&info, &mut names);
975        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
976
977        let range = &decoded.ranges[0];
978        assert_eq!(range.start, Position { line: 3, column: 5 });
979        assert_eq!(
980            range.end,
981            Position {
982                line: 3,
983                column: 25
984            }
985        );
986    }
987
988    #[test]
989    fn scopes_first_empty_second_populated() {
990        let info = ScopeInfo {
991            scopes: vec![
992                None, // First source has no scopes
993                Some(OriginalScope {
994                    start: Position { line: 0, column: 0 },
995                    end: Position { line: 5, column: 0 },
996                    name: None,
997                    kind: None,
998                    is_stack_frame: false,
999                    variables: vec![],
1000                    children: vec![],
1001                }),
1002            ],
1003            ranges: vec![],
1004        };
1005
1006        let mut names = vec![];
1007        let encoded = encode_scopes(&info, &mut names);
1008        let decoded = decode_scopes(&encoded, &names, 2).unwrap();
1009
1010        assert!(decoded.scopes[0].is_none());
1011        assert!(decoded.scopes[1].is_some());
1012    }
1013
1014    #[test]
1015    fn ranges_only_no_scopes_multi_source() {
1016        let info = ScopeInfo {
1017            scopes: vec![None, None],
1018            ranges: vec![GeneratedRange {
1019                start: Position { line: 0, column: 0 },
1020                end: Position { line: 5, column: 0 },
1021                is_stack_frame: false,
1022                is_hidden: false,
1023                definition: None,
1024                call_site: None,
1025                bindings: vec![],
1026                children: vec![],
1027            }],
1028        };
1029
1030        let mut names = vec![];
1031        let encoded = encode_scopes(&info, &mut names);
1032        let decoded = decode_scopes(&encoded, &names, 2).unwrap();
1033
1034        assert_eq!(decoded.scopes.len(), 2);
1035        assert!(decoded.scopes[0].is_none());
1036        assert!(decoded.scopes[1].is_none());
1037        assert_eq!(decoded.ranges.len(), 1);
1038    }
1039
1040    #[test]
1041    fn range_no_definition_explicit() {
1042        let info = ScopeInfo {
1043            scopes: vec![None],
1044            ranges: vec![GeneratedRange {
1045                start: Position { line: 0, column: 0 },
1046                end: Position { line: 5, column: 0 },
1047                is_stack_frame: false,
1048                is_hidden: false,
1049                definition: None,
1050                call_site: None,
1051                bindings: vec![],
1052                children: vec![],
1053            }],
1054        };
1055
1056        let mut names = vec![];
1057        let encoded = encode_scopes(&info, &mut names);
1058        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1059
1060        assert_eq!(decoded.ranges[0].definition, None);
1061    }
1062
1063    #[test]
1064    fn sub_range_same_line_bindings() {
1065        // Sub-ranges where positions are on the same line (column-only delta)
1066        let info = ScopeInfo {
1067            scopes: vec![Some(OriginalScope {
1068                start: Position { line: 0, column: 0 },
1069                end: Position {
1070                    line: 10,
1071                    column: 0,
1072                },
1073                name: None,
1074                kind: None,
1075                is_stack_frame: false,
1076                variables: vec!["x".to_string()],
1077                children: vec![],
1078            })],
1079            ranges: vec![GeneratedRange {
1080                start: Position { line: 0, column: 0 },
1081                end: Position {
1082                    line: 10,
1083                    column: 0,
1084                },
1085                is_stack_frame: false,
1086                is_hidden: false,
1087                definition: Some(0),
1088                call_site: None,
1089                bindings: vec![Binding::SubRanges(vec![
1090                    SubRangeBinding {
1091                        expression: Some("a".to_string()),
1092                        from: Position { line: 0, column: 0 },
1093                    },
1094                    SubRangeBinding {
1095                        expression: Some("b".to_string()),
1096                        from: Position {
1097                            line: 0,
1098                            column: 15,
1099                        },
1100                    },
1101                ])],
1102                children: vec![],
1103            }],
1104        };
1105
1106        let mut names = vec![];
1107        let encoded = encode_scopes(&info, &mut names);
1108        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1109
1110        match &decoded.ranges[0].bindings[0] {
1111            Binding::SubRanges(subs) => {
1112                assert_eq!(subs.len(), 2);
1113                assert_eq!(subs[0].from, Position { line: 0, column: 0 });
1114                assert_eq!(
1115                    subs[1].from,
1116                    Position {
1117                        line: 0,
1118                        column: 15
1119                    }
1120                );
1121            }
1122            other => panic!("expected SubRanges, got {other:?}"),
1123        }
1124    }
1125
1126    #[test]
1127    fn call_site_with_nonzero_values() {
1128        let info = ScopeInfo {
1129            scopes: vec![Some(OriginalScope {
1130                start: Position { line: 0, column: 0 },
1131                end: Position {
1132                    line: 20,
1133                    column: 0,
1134                },
1135                name: None,
1136                kind: None,
1137                is_stack_frame: false,
1138                variables: vec![],
1139                children: vec![],
1140            })],
1141            ranges: vec![GeneratedRange {
1142                start: Position { line: 0, column: 0 },
1143                end: Position {
1144                    line: 20,
1145                    column: 0,
1146                },
1147                is_stack_frame: false,
1148                is_hidden: false,
1149                definition: Some(0),
1150                call_site: Some(CallSite {
1151                    source_index: 2,
1152                    line: 15,
1153                    column: 8,
1154                }),
1155                bindings: vec![],
1156                children: vec![],
1157            }],
1158        };
1159
1160        let mut names = vec![];
1161        let encoded = encode_scopes(&info, &mut names);
1162        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1163
1164        let cs = decoded.ranges[0].call_site.as_ref().unwrap();
1165        assert_eq!(cs.source_index, 2);
1166        assert_eq!(cs.line, 15);
1167        assert_eq!(cs.column, 8);
1168    }
1169
1170    // ── Additional coverage tests ────────────────────────────────
1171
1172    #[test]
1173    fn scope_with_name_and_kind_roundtrip() {
1174        // Exercises OS_FLAG_HAS_NAME + OS_FLAG_HAS_KIND together
1175        // Covers decode.rs lines 139, 141, 143, 151, 159, 161
1176        let info = ScopeInfo {
1177            scopes: vec![Some(OriginalScope {
1178                start: Position { line: 2, column: 4 },
1179                end: Position {
1180                    line: 15,
1181                    column: 1,
1182                },
1183                name: Some("myFunc".to_string()),
1184                kind: Some("function".to_string()),
1185                is_stack_frame: true,
1186                variables: vec!["arg1".to_string(), "arg2".to_string()],
1187                children: vec![OriginalScope {
1188                    start: Position { line: 3, column: 8 },
1189                    end: Position {
1190                        line: 14,
1191                        column: 5,
1192                    },
1193                    name: Some("innerBlock".to_string()),
1194                    kind: Some("block".to_string()),
1195                    is_stack_frame: false,
1196                    variables: vec!["tmp".to_string()],
1197                    children: vec![],
1198                }],
1199            })],
1200            ranges: vec![],
1201        };
1202
1203        let mut names = vec![];
1204        let encoded = encode_scopes(&info, &mut names);
1205        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1206
1207        let scope = decoded.scopes[0].as_ref().unwrap();
1208        assert_eq!(scope.name.as_deref(), Some("myFunc"));
1209        assert_eq!(scope.kind.as_deref(), Some("function"));
1210        assert!(scope.is_stack_frame);
1211        assert_eq!(scope.variables, vec!["arg1", "arg2"]);
1212
1213        let child = &scope.children[0];
1214        assert_eq!(child.name.as_deref(), Some("innerBlock"));
1215        assert_eq!(child.kind.as_deref(), Some("block"));
1216        assert!(!child.is_stack_frame);
1217        assert_eq!(child.variables, vec!["tmp"]);
1218    }
1219
1220    #[test]
1221    fn range_end_multiline_2vlq() {
1222        // Range where end is on a different line than start, producing a
1223        // 2-VLQ range end (line_delta + column).
1224        // Covers decode.rs lines 282, 286, 288 (TAG_GENERATED_RANGE_END with 2 VLQs)
1225        let info = ScopeInfo {
1226            scopes: vec![],
1227            ranges: vec![GeneratedRange {
1228                start: Position { line: 0, column: 0 },
1229                end: Position {
1230                    line: 7,
1231                    column: 15,
1232                },
1233                is_stack_frame: false,
1234                is_hidden: false,
1235                definition: None,
1236                call_site: None,
1237                bindings: vec![],
1238                children: vec![GeneratedRange {
1239                    start: Position { line: 1, column: 5 },
1240                    end: Position {
1241                        line: 4,
1242                        column: 10,
1243                    },
1244                    is_stack_frame: false,
1245                    is_hidden: false,
1246                    definition: None,
1247                    call_site: None,
1248                    bindings: vec![],
1249                    children: vec![],
1250                }],
1251            }],
1252        };
1253
1254        let mut names = vec![];
1255        let encoded = encode_scopes(&info, &mut names);
1256        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
1257
1258        let outer = &decoded.ranges[0];
1259        assert_eq!(
1260            outer.end,
1261            Position {
1262                line: 7,
1263                column: 15
1264            }
1265        );
1266        let inner = &outer.children[0];
1267        assert_eq!(inner.start, Position { line: 1, column: 5 });
1268        assert_eq!(
1269            inner.end,
1270            Position {
1271                line: 4,
1272                column: 10
1273            }
1274        );
1275    }
1276
1277    #[test]
1278    fn binding_unavailable_roundtrip() {
1279        // Exercises Binding::Unavailable (binding idx = 0) path
1280        // Covers decode.rs lines 333, 340 (TAG_GENERATED_RANGE_BINDINGS with idx=0)
1281        let info = ScopeInfo {
1282            scopes: vec![Some(OriginalScope {
1283                start: Position { line: 0, column: 0 },
1284                end: Position {
1285                    line: 10,
1286                    column: 0,
1287                },
1288                name: None,
1289                kind: None,
1290                is_stack_frame: false,
1291                variables: vec!["a".to_string(), "b".to_string(), "c".to_string()],
1292                children: vec![],
1293            })],
1294            ranges: vec![GeneratedRange {
1295                start: Position { line: 0, column: 0 },
1296                end: Position {
1297                    line: 10,
1298                    column: 0,
1299                },
1300                is_stack_frame: false,
1301                is_hidden: false,
1302                definition: Some(0),
1303                call_site: None,
1304                bindings: vec![
1305                    Binding::Unavailable,
1306                    Binding::Expression("_b".to_string()),
1307                    Binding::Unavailable,
1308                ],
1309                children: vec![],
1310            }],
1311        };
1312
1313        let mut names = vec![];
1314        let encoded = encode_scopes(&info, &mut names);
1315        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1316
1317        assert_eq!(decoded.ranges[0].bindings.len(), 3);
1318        assert_eq!(decoded.ranges[0].bindings[0], Binding::Unavailable);
1319        assert_eq!(
1320            decoded.ranges[0].bindings[1],
1321            Binding::Expression("_b".to_string())
1322        );
1323        assert_eq!(decoded.ranges[0].bindings[2], Binding::Unavailable);
1324    }
1325
1326    #[test]
1327    fn sub_range_with_none_expression() {
1328        // Sub-range bindings where a sub-range has expression = None (Unavailable)
1329        // Covers encode.rs lines 267, 271 (None expression → emit 0)
1330        // and decode.rs lines 353, 354, 357, 364 (sub-range reading)
1331        let info = ScopeInfo {
1332            scopes: vec![Some(OriginalScope {
1333                start: Position { line: 0, column: 0 },
1334                end: Position {
1335                    line: 20,
1336                    column: 0,
1337                },
1338                name: None,
1339                kind: None,
1340                is_stack_frame: false,
1341                variables: vec!["x".to_string()],
1342                children: vec![],
1343            })],
1344            ranges: vec![GeneratedRange {
1345                start: Position { line: 0, column: 0 },
1346                end: Position {
1347                    line: 20,
1348                    column: 0,
1349                },
1350                is_stack_frame: false,
1351                is_hidden: false,
1352                definition: Some(0),
1353                call_site: None,
1354                bindings: vec![Binding::SubRanges(vec![
1355                    SubRangeBinding {
1356                        expression: Some("a".to_string()),
1357                        from: Position { line: 0, column: 0 },
1358                    },
1359                    SubRangeBinding {
1360                        expression: None,
1361                        from: Position { line: 5, column: 0 },
1362                    },
1363                    SubRangeBinding {
1364                        expression: Some("c".to_string()),
1365                        from: Position {
1366                            line: 10,
1367                            column: 0,
1368                        },
1369                    },
1370                ])],
1371                children: vec![],
1372            }],
1373        };
1374
1375        let mut names = vec![];
1376        let encoded = encode_scopes(&info, &mut names);
1377        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1378
1379        match &decoded.ranges[0].bindings[0] {
1380            Binding::SubRanges(subs) => {
1381                assert_eq!(subs.len(), 3);
1382                assert_eq!(subs[0].expression.as_deref(), Some("a"));
1383                assert_eq!(subs[1].expression, None);
1384                assert_eq!(subs[2].expression.as_deref(), Some("c"));
1385            }
1386            other => panic!("expected SubRanges, got {other:?}"),
1387        }
1388    }
1389
1390    #[test]
1391    fn sub_range_multiple_variables() {
1392        // Multiple variables each with sub-ranges, exercises the H tag
1393        // encode/decode with h_first handling (encode.rs line 290)
1394        // and decode.rs lines 353-375 (TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS)
1395        let info = ScopeInfo {
1396            scopes: vec![Some(OriginalScope {
1397                start: Position { line: 0, column: 0 },
1398                end: Position {
1399                    line: 30,
1400                    column: 0,
1401                },
1402                name: None,
1403                kind: None,
1404                is_stack_frame: false,
1405                variables: vec!["x".to_string(), "y".to_string(), "z".to_string()],
1406                children: vec![],
1407            })],
1408            ranges: vec![GeneratedRange {
1409                start: Position { line: 0, column: 0 },
1410                end: Position {
1411                    line: 30,
1412                    column: 0,
1413                },
1414                is_stack_frame: false,
1415                is_hidden: false,
1416                definition: Some(0),
1417                call_site: None,
1418                bindings: vec![
1419                    // Variable 0 (x): sub-ranges
1420                    Binding::SubRanges(vec![
1421                        SubRangeBinding {
1422                            expression: Some("_x1".to_string()),
1423                            from: Position { line: 0, column: 0 },
1424                        },
1425                        SubRangeBinding {
1426                            expression: Some("_x2".to_string()),
1427                            from: Position {
1428                                line: 10,
1429                                column: 0,
1430                            },
1431                        },
1432                    ]),
1433                    // Variable 1 (y): simple binding
1434                    Binding::Expression("_y".to_string()),
1435                    // Variable 2 (z): sub-ranges (second H item, exercises h_first=false)
1436                    Binding::SubRanges(vec![
1437                        SubRangeBinding {
1438                            expression: Some("_z1".to_string()),
1439                            from: Position { line: 0, column: 0 },
1440                        },
1441                        SubRangeBinding {
1442                            expression: None,
1443                            from: Position {
1444                                line: 15,
1445                                column: 5,
1446                            },
1447                        },
1448                        SubRangeBinding {
1449                            expression: Some("_z3".to_string()),
1450                            from: Position {
1451                                line: 20,
1452                                column: 0,
1453                            },
1454                        },
1455                    ]),
1456                ],
1457                children: vec![],
1458            }],
1459        };
1460
1461        let mut names = vec![];
1462        let encoded = encode_scopes(&info, &mut names);
1463        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1464
1465        let bindings = &decoded.ranges[0].bindings;
1466        assert_eq!(bindings.len(), 3);
1467
1468        // Variable 0: sub-ranges
1469        match &bindings[0] {
1470            Binding::SubRanges(subs) => {
1471                assert_eq!(subs.len(), 2);
1472                assert_eq!(subs[0].expression.as_deref(), Some("_x1"));
1473                assert_eq!(subs[1].expression.as_deref(), Some("_x2"));
1474                assert_eq!(
1475                    subs[1].from,
1476                    Position {
1477                        line: 10,
1478                        column: 0
1479                    }
1480                );
1481            }
1482            other => panic!("expected SubRanges for x, got {other:?}"),
1483        }
1484
1485        // Variable 1: simple expression
1486        assert_eq!(bindings[1], Binding::Expression("_y".to_string()));
1487
1488        // Variable 2: sub-ranges (second H item)
1489        match &bindings[2] {
1490            Binding::SubRanges(subs) => {
1491                assert_eq!(subs.len(), 3);
1492                assert_eq!(subs[0].expression.as_deref(), Some("_z1"));
1493                assert_eq!(subs[1].expression, None);
1494                assert_eq!(
1495                    subs[1].from,
1496                    Position {
1497                        line: 15,
1498                        column: 5
1499                    }
1500                );
1501                assert_eq!(subs[2].expression.as_deref(), Some("_z3"));
1502            }
1503            other => panic!("expected SubRanges for z, got {other:?}"),
1504        }
1505    }
1506
1507    #[test]
1508    fn call_site_on_standalone_range() {
1509        // Range with call_site but also a definition (typical inlining pattern).
1510        // Exercises decode.rs lines 380-388 (TAG_GENERATED_RANGE_CALL_SITE)
1511        let info = ScopeInfo {
1512            scopes: vec![Some(OriginalScope {
1513                start: Position { line: 0, column: 0 },
1514                end: Position {
1515                    line: 30,
1516                    column: 0,
1517                },
1518                name: None,
1519                kind: Some("global".to_string()),
1520                is_stack_frame: false,
1521                variables: vec![],
1522                children: vec![OriginalScope {
1523                    start: Position { line: 5, column: 0 },
1524                    end: Position {
1525                        line: 10,
1526                        column: 1,
1527                    },
1528                    name: Some("inlined".to_string()),
1529                    kind: Some("function".to_string()),
1530                    is_stack_frame: true,
1531                    variables: vec!["p".to_string()],
1532                    children: vec![],
1533                }],
1534            })],
1535            ranges: vec![GeneratedRange {
1536                start: Position { line: 0, column: 0 },
1537                end: Position {
1538                    line: 30,
1539                    column: 0,
1540                },
1541                is_stack_frame: false,
1542                is_hidden: false,
1543                definition: Some(0),
1544                call_site: None,
1545                bindings: vec![],
1546                children: vec![GeneratedRange {
1547                    start: Position {
1548                        line: 12,
1549                        column: 0,
1550                    },
1551                    end: Position {
1552                        line: 18,
1553                        column: 0,
1554                    },
1555                    is_stack_frame: true,
1556                    is_hidden: false,
1557                    definition: Some(1),
1558                    call_site: Some(CallSite {
1559                        source_index: 0,
1560                        line: 20,
1561                        column: 4,
1562                    }),
1563                    bindings: vec![Binding::Expression("arg0".to_string())],
1564                    children: vec![],
1565                }],
1566            }],
1567        };
1568
1569        let mut names = vec![];
1570        let encoded = encode_scopes(&info, &mut names);
1571        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1572
1573        let inner = &decoded.ranges[0].children[0];
1574        assert!(inner.is_stack_frame);
1575        assert_eq!(inner.definition, Some(1));
1576        let cs = inner.call_site.as_ref().unwrap();
1577        assert_eq!(cs.source_index, 0);
1578        assert_eq!(cs.line, 20);
1579        assert_eq!(cs.column, 4);
1580        assert_eq!(
1581            inner.bindings,
1582            vec![Binding::Expression("arg0".to_string())]
1583        );
1584    }
1585
1586    #[test]
1587    fn unknown_tag_skipped() {
1588        // Manually craft a string with an unknown tag (e.g., tag 0x9) followed
1589        // by some VLQs, then a valid scope.
1590        // This exercises decode.rs lines 393, 394 (unknown tag skip).
1591        //
1592        // First encode a simple scope, then prepend an unknown tag item.
1593        let info = ScopeInfo {
1594            scopes: vec![Some(OriginalScope {
1595                start: Position { line: 0, column: 0 },
1596                end: Position { line: 5, column: 0 },
1597                name: None,
1598                kind: None,
1599                is_stack_frame: false,
1600                variables: vec![],
1601                children: vec![],
1602            })],
1603            ranges: vec![],
1604        };
1605
1606        let mut names = vec![];
1607        let encoded = encode_scopes(&info, &mut names);
1608
1609        // Build unknown tag item: tag 0x12 (= 18, unused) followed by two
1610        // unsigned VLQs, then comma, then the valid encoded data.
1611        let mut crafted = Vec::new();
1612        srcmap_codec::vlq_encode_unsigned(&mut crafted, 18); // unknown tag
1613        srcmap_codec::vlq_encode_unsigned(&mut crafted, 42); // dummy VLQ 1
1614        srcmap_codec::vlq_encode_unsigned(&mut crafted, 7); // dummy VLQ 2
1615        crafted.push(b',');
1616        crafted.extend_from_slice(encoded.as_bytes());
1617
1618        let crafted_str = std::str::from_utf8(&crafted).unwrap();
1619        let decoded = decode_scopes(crafted_str, &names, 1).unwrap();
1620
1621        assert_eq!(decoded.scopes.len(), 1);
1622        assert!(decoded.scopes[0].is_some());
1623        let scope = decoded.scopes[0].as_ref().unwrap();
1624        assert_eq!(scope.start, Position { line: 0, column: 0 });
1625        assert_eq!(scope.end, Position { line: 5, column: 0 });
1626    }
1627
1628    #[test]
1629    fn first_source_none_exercises_empty_path() {
1630        // First source has no scopes (None), second has scopes.
1631        // This exercises encode.rs line 89 (first_item && scope.is_none())
1632        // and decode.rs lines 124, 129 (empty item + skip_comma)
1633        let info = ScopeInfo {
1634            scopes: vec![
1635                None,
1636                None,
1637                Some(OriginalScope {
1638                    start: Position { line: 0, column: 0 },
1639                    end: Position { line: 5, column: 0 },
1640                    name: None,
1641                    kind: None,
1642                    is_stack_frame: false,
1643                    variables: vec![],
1644                    children: vec![],
1645                }),
1646            ],
1647            ranges: vec![],
1648        };
1649
1650        let mut names = vec![];
1651        let encoded = encode_scopes(&info, &mut names);
1652        let decoded = decode_scopes(&encoded, &names, 3).unwrap();
1653
1654        assert_eq!(decoded.scopes.len(), 3);
1655        assert!(decoded.scopes[0].is_none());
1656        assert!(decoded.scopes[1].is_none());
1657        assert!(decoded.scopes[2].is_some());
1658    }
1659
1660    #[test]
1661    fn scope_end_same_line_as_child_end() {
1662        // Parent scope ends on the same line where child scope ended.
1663        // This exercises the column-relative path in TAG_ORIGINAL_SCOPE_END
1664        // (decode.rs lines 183, 186, 188 where line_delta = 0)
1665        let info = ScopeInfo {
1666            scopes: vec![Some(OriginalScope {
1667                start: Position { line: 0, column: 0 },
1668                end: Position {
1669                    line: 10,
1670                    column: 20,
1671                },
1672                name: None,
1673                kind: None,
1674                is_stack_frame: false,
1675                variables: vec![],
1676                children: vec![OriginalScope {
1677                    start: Position { line: 5, column: 0 },
1678                    end: Position {
1679                        line: 10,
1680                        column: 10,
1681                    },
1682                    name: None,
1683                    kind: None,
1684                    is_stack_frame: false,
1685                    variables: vec![],
1686                    children: vec![],
1687                }],
1688            })],
1689            ranges: vec![],
1690        };
1691
1692        let mut names = vec![];
1693        let encoded = encode_scopes(&info, &mut names);
1694        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1695
1696        let scope = decoded.scopes[0].as_ref().unwrap();
1697        assert_eq!(
1698            scope.end,
1699            Position {
1700                line: 10,
1701                column: 20
1702            }
1703        );
1704        assert_eq!(
1705            scope.children[0].end,
1706            Position {
1707                line: 10,
1708                column: 10
1709            }
1710        );
1711    }
1712
1713    #[test]
1714    fn generated_range_has_line_flag() {
1715        // Range starting on a non-zero line (exercises GR_FLAG_HAS_LINE)
1716        // Covers decode.rs lines 232, 233 (has_line flag, read line delta)
1717        // and encode.rs line_delta != 0 path
1718        let info = ScopeInfo {
1719            scopes: vec![],
1720            ranges: vec![
1721                GeneratedRange {
1722                    start: Position { line: 0, column: 5 },
1723                    end: Position {
1724                        line: 0,
1725                        column: 50,
1726                    },
1727                    is_stack_frame: false,
1728                    is_hidden: false,
1729                    definition: None,
1730                    call_site: None,
1731                    bindings: vec![],
1732                    children: vec![],
1733                },
1734                GeneratedRange {
1735                    start: Position {
1736                        line: 3,
1737                        column: 10,
1738                    },
1739                    end: Position {
1740                        line: 8,
1741                        column: 20,
1742                    },
1743                    is_stack_frame: false,
1744                    is_hidden: false,
1745                    definition: None,
1746                    call_site: None,
1747                    bindings: vec![],
1748                    children: vec![],
1749                },
1750            ],
1751        };
1752
1753        let mut names = vec![];
1754        let encoded = encode_scopes(&info, &mut names);
1755        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
1756
1757        assert_eq!(decoded.ranges.len(), 2);
1758        assert_eq!(decoded.ranges[0].start, Position { line: 0, column: 5 });
1759        assert_eq!(
1760            decoded.ranges[0].end,
1761            Position {
1762                line: 0,
1763                column: 50
1764            }
1765        );
1766        assert_eq!(
1767            decoded.ranges[1].start,
1768            Position {
1769                line: 3,
1770                column: 10
1771            }
1772        );
1773        assert_eq!(
1774            decoded.ranges[1].end,
1775            Position {
1776                line: 8,
1777                column: 20
1778            }
1779        );
1780    }
1781
1782    #[test]
1783    fn scope_variables_decode_path() {
1784        // Scope with variables on a child to exercise TAG_ORIGINAL_SCOPE_VARIABLES
1785        // decode path (decode.rs lines 221, 223, 225)
1786        let info = ScopeInfo {
1787            scopes: vec![Some(OriginalScope {
1788                start: Position { line: 0, column: 0 },
1789                end: Position {
1790                    line: 20,
1791                    column: 0,
1792                },
1793                name: None,
1794                kind: None,
1795                is_stack_frame: false,
1796                variables: vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()],
1797                children: vec![OriginalScope {
1798                    start: Position { line: 2, column: 0 },
1799                    end: Position {
1800                        line: 18,
1801                        column: 0,
1802                    },
1803                    name: None,
1804                    kind: None,
1805                    is_stack_frame: false,
1806                    variables: vec!["delta".to_string(), "epsilon".to_string()],
1807                    children: vec![],
1808                }],
1809            })],
1810            ranges: vec![],
1811        };
1812
1813        let mut names = vec![];
1814        let encoded = encode_scopes(&info, &mut names);
1815        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1816
1817        let scope = decoded.scopes[0].as_ref().unwrap();
1818        assert_eq!(scope.variables, vec!["alpha", "beta", "gamma"]);
1819        assert_eq!(scope.children[0].variables, vec!["delta", "epsilon"]);
1820    }
1821
1822    #[test]
1823    fn sub_range_first_expression_none() {
1824        // Sub-ranges where the first expression is None (Unavailable at range start).
1825        // This exercises encode.rs line 267 (first sub expression is None → emit 0 in G)
1826        let info = ScopeInfo {
1827            scopes: vec![Some(OriginalScope {
1828                start: Position { line: 0, column: 0 },
1829                end: Position {
1830                    line: 20,
1831                    column: 0,
1832                },
1833                name: None,
1834                kind: None,
1835                is_stack_frame: false,
1836                variables: vec!["v".to_string()],
1837                children: vec![],
1838            })],
1839            ranges: vec![GeneratedRange {
1840                start: Position { line: 0, column: 0 },
1841                end: Position {
1842                    line: 20,
1843                    column: 0,
1844                },
1845                is_stack_frame: false,
1846                is_hidden: false,
1847                definition: Some(0),
1848                call_site: None,
1849                bindings: vec![Binding::SubRanges(vec![
1850                    SubRangeBinding {
1851                        expression: None,
1852                        from: Position { line: 0, column: 0 },
1853                    },
1854                    SubRangeBinding {
1855                        expression: Some("_v".to_string()),
1856                        from: Position { line: 8, column: 0 },
1857                    },
1858                ])],
1859                children: vec![],
1860            }],
1861        };
1862
1863        let mut names = vec![];
1864        let encoded = encode_scopes(&info, &mut names);
1865        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
1866
1867        match &decoded.ranges[0].bindings[0] {
1868            Binding::SubRanges(subs) => {
1869                assert_eq!(subs.len(), 2);
1870                assert_eq!(subs[0].expression, None);
1871                assert_eq!(subs[0].from, Position { line: 0, column: 0 });
1872                assert_eq!(subs[1].expression.as_deref(), Some("_v"));
1873                assert_eq!(subs[1].from, Position { line: 8, column: 0 });
1874            }
1875            other => panic!("expected SubRanges, got {other:?}"),
1876        }
1877    }
1878
1879    #[test]
1880    fn comprehensive_roundtrip() {
1881        // Full roundtrip combining scopes with name+kind, nested ranges with
1882        // call sites, sub-range bindings, unavailable bindings, and hidden ranges.
1883        let info = ScopeInfo {
1884            scopes: vec![
1885                Some(OriginalScope {
1886                    start: Position { line: 0, column: 0 },
1887                    end: Position {
1888                        line: 50,
1889                        column: 0,
1890                    },
1891                    name: None,
1892                    kind: Some("module".to_string()),
1893                    is_stack_frame: false,
1894                    variables: vec!["exports".to_string()],
1895                    children: vec![
1896                        OriginalScope {
1897                            start: Position { line: 2, column: 0 },
1898                            end: Position {
1899                                line: 20,
1900                                column: 1,
1901                            },
1902                            name: Some("add".to_string()),
1903                            kind: Some("function".to_string()),
1904                            is_stack_frame: true,
1905                            variables: vec!["a".to_string(), "b".to_string()],
1906                            children: vec![],
1907                        },
1908                        OriginalScope {
1909                            start: Position {
1910                                line: 22,
1911                                column: 0,
1912                            },
1913                            end: Position {
1914                                line: 40,
1915                                column: 1,
1916                            },
1917                            name: Some("multiply".to_string()),
1918                            kind: Some("function".to_string()),
1919                            is_stack_frame: true,
1920                            variables: vec!["x".to_string(), "y".to_string()],
1921                            children: vec![],
1922                        },
1923                    ],
1924                }),
1925                None, // second source: no scopes
1926            ],
1927            ranges: vec![GeneratedRange {
1928                start: Position { line: 0, column: 0 },
1929                end: Position {
1930                    line: 25,
1931                    column: 0,
1932                },
1933                is_stack_frame: false,
1934                is_hidden: false,
1935                definition: Some(0),
1936                call_site: None,
1937                bindings: vec![Binding::Expression("module.exports".to_string())],
1938                children: vec![
1939                    GeneratedRange {
1940                        start: Position { line: 1, column: 0 },
1941                        end: Position {
1942                            line: 10,
1943                            column: 0,
1944                        },
1945                        is_stack_frame: true,
1946                        is_hidden: false,
1947                        definition: Some(1),
1948                        call_site: Some(CallSite {
1949                            source_index: 0,
1950                            line: 45,
1951                            column: 2,
1952                        }),
1953                        bindings: vec![Binding::Expression("_a".to_string()), Binding::Unavailable],
1954                        children: vec![],
1955                    },
1956                    GeneratedRange {
1957                        start: Position {
1958                            line: 12,
1959                            column: 0,
1960                        },
1961                        end: Position {
1962                            line: 20,
1963                            column: 0,
1964                        },
1965                        is_stack_frame: true,
1966                        is_hidden: true,
1967                        definition: Some(2),
1968                        call_site: Some(CallSite {
1969                            source_index: 0,
1970                            line: 46,
1971                            column: 0,
1972                        }),
1973                        bindings: vec![
1974                            Binding::SubRanges(vec![
1975                                SubRangeBinding {
1976                                    expression: Some("p1".to_string()),
1977                                    from: Position {
1978                                        line: 12,
1979                                        column: 0,
1980                                    },
1981                                },
1982                                SubRangeBinding {
1983                                    expression: Some("p2".to_string()),
1984                                    from: Position {
1985                                        line: 16,
1986                                        column: 0,
1987                                    },
1988                                },
1989                            ]),
1990                            Binding::Expression("_y".to_string()),
1991                        ],
1992                        children: vec![],
1993                    },
1994                ],
1995            }],
1996        };
1997
1998        let mut names = vec![];
1999        let encoded = encode_scopes(&info, &mut names);
2000        let decoded = decode_scopes(&encoded, &names, 2).unwrap();
2001
2002        // Verify scopes
2003        assert_eq!(decoded.scopes.len(), 2);
2004        assert!(decoded.scopes[1].is_none());
2005        let root = decoded.scopes[0].as_ref().unwrap();
2006        assert_eq!(root.kind.as_deref(), Some("module"));
2007        assert_eq!(root.children.len(), 2);
2008        assert_eq!(root.children[0].name.as_deref(), Some("add"));
2009        assert_eq!(root.children[1].name.as_deref(), Some("multiply"));
2010
2011        // Verify ranges
2012        assert_eq!(decoded.ranges.len(), 1);
2013        let outer = &decoded.ranges[0];
2014        assert_eq!(outer.children.len(), 2);
2015
2016        // First child: inlined add
2017        let add_range = &outer.children[0];
2018        assert!(add_range.is_stack_frame);
2019        assert!(!add_range.is_hidden);
2020        assert_eq!(add_range.definition, Some(1));
2021        assert_eq!(
2022            add_range.call_site,
2023            Some(CallSite {
2024                source_index: 0,
2025                line: 45,
2026                column: 2
2027            })
2028        );
2029        assert_eq!(add_range.bindings[0], Binding::Expression("_a".to_string()));
2030        assert_eq!(add_range.bindings[1], Binding::Unavailable);
2031
2032        // Second child: inlined multiply (hidden)
2033        let mul_range = &outer.children[1];
2034        assert!(mul_range.is_stack_frame);
2035        assert!(mul_range.is_hidden);
2036        assert_eq!(mul_range.definition, Some(2));
2037        match &mul_range.bindings[0] {
2038            Binding::SubRanges(subs) => {
2039                assert_eq!(subs.len(), 2);
2040                assert_eq!(subs[0].expression.as_deref(), Some("p1"));
2041                assert_eq!(subs[1].expression.as_deref(), Some("p2"));
2042            }
2043            other => panic!("expected SubRanges, got {other:?}"),
2044        }
2045        assert_eq!(mul_range.bindings[1], Binding::Expression("_y".to_string()));
2046    }
2047
2048    #[test]
2049    fn range_end_column_only_1vlq() {
2050        // Range where end is on the same line as a previous position, resulting
2051        // in a 1-VLQ range end (column only). The parent range ending on line 5
2052        // after the child also ended on line 5.
2053        let info = ScopeInfo {
2054            scopes: vec![],
2055            ranges: vec![GeneratedRange {
2056                start: Position { line: 0, column: 0 },
2057                end: Position {
2058                    line: 5,
2059                    column: 50,
2060                },
2061                is_stack_frame: false,
2062                is_hidden: false,
2063                definition: None,
2064                call_site: None,
2065                bindings: vec![],
2066                children: vec![GeneratedRange {
2067                    start: Position { line: 2, column: 0 },
2068                    end: Position {
2069                        line: 5,
2070                        column: 30,
2071                    },
2072                    is_stack_frame: false,
2073                    is_hidden: false,
2074                    definition: None,
2075                    call_site: None,
2076                    bindings: vec![],
2077                    children: vec![],
2078                }],
2079            }],
2080        };
2081
2082        let mut names = vec![];
2083        let encoded = encode_scopes(&info, &mut names);
2084        let decoded = decode_scopes(&encoded, &names, 0).unwrap();
2085
2086        let outer = &decoded.ranges[0];
2087        assert_eq!(
2088            outer.end,
2089            Position {
2090                line: 5,
2091                column: 50
2092            }
2093        );
2094        let inner = &outer.children[0];
2095        assert_eq!(
2096            inner.end,
2097            Position {
2098                line: 5,
2099                column: 30
2100            }
2101        );
2102    }
2103
2104    #[test]
2105    fn all_sources_none_with_ranges() {
2106        // All sources have None scopes, but ranges exist.
2107        // Exercises the transition to in_generated_ranges when scopes are all None.
2108        let info = ScopeInfo {
2109            scopes: vec![None, None, None],
2110            ranges: vec![GeneratedRange {
2111                start: Position { line: 0, column: 0 },
2112                end: Position {
2113                    line: 10,
2114                    column: 0,
2115                },
2116                is_stack_frame: false,
2117                is_hidden: false,
2118                definition: None,
2119                call_site: None,
2120                bindings: vec![],
2121                children: vec![],
2122            }],
2123        };
2124
2125        let mut names = vec![];
2126        let encoded = encode_scopes(&info, &mut names);
2127        let decoded = decode_scopes(&encoded, &names, 3).unwrap();
2128
2129        assert_eq!(decoded.scopes.len(), 3);
2130        assert!(decoded.scopes.iter().all(|s| s.is_none()));
2131        assert_eq!(decoded.ranges.len(), 1);
2132    }
2133
2134    #[test]
2135    fn scope_name_only_no_kind() {
2136        // Scope with name but no kind (exercises OS_FLAG_HAS_NAME without OS_FLAG_HAS_KIND)
2137        let info = ScopeInfo {
2138            scopes: vec![Some(OriginalScope {
2139                start: Position { line: 0, column: 0 },
2140                end: Position { line: 5, column: 0 },
2141                name: Some("myVar".to_string()),
2142                kind: None,
2143                is_stack_frame: false,
2144                variables: vec![],
2145                children: vec![],
2146            })],
2147            ranges: vec![],
2148        };
2149
2150        let mut names = vec![];
2151        let encoded = encode_scopes(&info, &mut names);
2152        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
2153
2154        let scope = decoded.scopes[0].as_ref().unwrap();
2155        assert_eq!(scope.name.as_deref(), Some("myVar"));
2156        assert_eq!(scope.kind, None);
2157    }
2158
2159    #[test]
2160    fn generated_range_with_definition_on_nonzero_line() {
2161        // Exercises GR_FLAG_HAS_LINE | GR_FLAG_HAS_DEFINITION together
2162        // Covers decode.rs lines 238, 241, 247, 255
2163        let info = ScopeInfo {
2164            scopes: vec![Some(OriginalScope {
2165                start: Position { line: 0, column: 0 },
2166                end: Position {
2167                    line: 50,
2168                    column: 0,
2169                },
2170                name: None,
2171                kind: None,
2172                is_stack_frame: false,
2173                variables: vec![],
2174                children: vec![],
2175            })],
2176            ranges: vec![GeneratedRange {
2177                start: Position {
2178                    line: 10,
2179                    column: 5,
2180                },
2181                end: Position {
2182                    line: 40,
2183                    column: 0,
2184                },
2185                is_stack_frame: true,
2186                is_hidden: false,
2187                definition: Some(0),
2188                call_site: None,
2189                bindings: vec![],
2190                children: vec![],
2191            }],
2192        };
2193
2194        let mut names = vec![];
2195        let encoded = encode_scopes(&info, &mut names);
2196        let decoded = decode_scopes(&encoded, &names, 1).unwrap();
2197
2198        let range = &decoded.ranges[0];
2199        assert_eq!(
2200            range.start,
2201            Position {
2202                line: 10,
2203                column: 5
2204            }
2205        );
2206        assert_eq!(
2207            range.end,
2208            Position {
2209                line: 40,
2210                column: 0
2211            }
2212        );
2213        assert!(range.is_stack_frame);
2214        assert_eq!(range.definition, Some(0));
2215    }
2216
2217    // ── Error path tests ───────────────────────────────────────────
2218
2219    #[test]
2220    fn decode_unmatched_scope_end() {
2221        // Craft a raw string with TAG_ORIGINAL_SCOPE_END (0x2 = 'C') without preceding start
2222        // TAG=2 (C), line_delta=0 (A), col=0 (A)
2223        let raw = "CAA";
2224        let names: Vec<String> = vec![];
2225        let err = decode_scopes(raw, &names, 1).unwrap_err();
2226        assert!(matches!(err, ScopesError::UnmatchedScopeEnd));
2227    }
2228
2229    #[test]
2230    fn decode_unclosed_scope() {
2231        // TAG_ORIGINAL_SCOPE_START (0x1 = 'B'), flags=0 (A), line=0 (A), col=0 (A)
2232        // No matching end tag
2233        let raw = "BAAA";
2234        let names: Vec<String> = vec![];
2235        let err = decode_scopes(raw, &names, 1).unwrap_err();
2236        assert!(matches!(err, ScopesError::UnclosedScope));
2237    }
2238
2239    #[test]
2240    fn decode_unmatched_range_end() {
2241        // First fill 1 source scope (empty) with comma separator
2242        // Then TAG_GENERATED_RANGE_END (0x5 = 'F'), col=0 (A)
2243        // Empty scope for source 0, then comma, then range end tag
2244        let raw = ",FA";
2245        let names: Vec<String> = vec![];
2246        let err = decode_scopes(raw, &names, 1).unwrap_err();
2247        assert!(matches!(err, ScopesError::UnmatchedRangeEnd));
2248    }
2249
2250    #[test]
2251    fn decode_unclosed_range() {
2252        // Fill 1 source (empty), then start a range without closing it
2253        // TAG_GENERATED_RANGE_START (0x4 = 'E'), flags=0 (A), col=0 (A)
2254        let raw = ",EAAA";
2255        let names: Vec<String> = vec![];
2256        let err = decode_scopes(raw, &names, 1).unwrap_err();
2257        assert!(matches!(err, ScopesError::UnclosedRange));
2258    }
2259}