Skip to main content

ScopeInfo

Struct ScopeInfo 

Source
pub struct ScopeInfo {
    pub scopes: Vec<Option<OriginalScope>>,
    pub ranges: Vec<GeneratedRange>,
}
Expand description

Decoded scope information from a source map.

Fields§

§scopes: Vec<Option<OriginalScope>>

Original scope trees, one per source file (aligned with sources). None means no scope info for that source file.

§ranges: Vec<GeneratedRange>

Top-level generated ranges for the output code.

Implementations§

Source§

impl ScopeInfo

Source

pub fn original_scope_for_definition( &self, definition: u32, ) -> Option<&OriginalScope>

Get the original scope referenced by a generated range’s definition index.

The definition index references scopes in pre-order traversal order across all source files.

Examples found in repository?
examples/debug_scopes.rs (line 297)
40fn main() {
41    // -----------------------------------------------------------------------
42    // 1. Build the original scope tree (what the author wrote)
43    // -----------------------------------------------------------------------
44    //
45    // ECMA-426 original scopes form a tree per source file. Each scope has:
46    //   - start/end positions (0-based line and column)
47    //   - an optional name (for named functions, classes, etc.)
48    //   - an optional kind ("global", "module", "function", "block", "class")
49    //   - is_stack_frame: true for function-like scopes that appear in call stacks
50    //   - variables: names declared in this scope (parameters, let/const/var)
51    //   - children: nested scopes
52    //
53    // Scopes are indexed by a pre-order traversal counter ("definition index"):
54    //   - definition 0: the module scope (root)
55    //   - definition 1: the `add` function scope (first child)
56
57    let add_scope = OriginalScope {
58        start: Position { line: 1, column: 0 },
59        end: Position { line: 3, column: 1 },
60        name: Some("add".to_string()),
61        kind: Some("function".to_string()),
62        is_stack_frame: true,
63        variables: vec!["a".to_string(), "b".to_string()],
64        children: vec![],
65    };
66
67    let module_scope = OriginalScope {
68        start: Position { line: 0, column: 0 },
69        end: Position {
70            line: 5,
71            column: 27,
72        },
73        name: None,
74        kind: Some("module".to_string()),
75        is_stack_frame: false,
76        variables: vec!["result".to_string()],
77        children: vec![add_scope],
78    };
79
80    // -----------------------------------------------------------------------
81    // 2. Build the generated ranges (what the bundler produced)
82    // -----------------------------------------------------------------------
83    //
84    // Generated ranges describe regions of the output code and how they map
85    // back to original scopes. Key fields:
86    //
87    //   - definition: index into the pre-order list of all original scopes,
88    //     linking this range to its corresponding original scope
89    //   - call_site: if this range is an inlined function body, the location
90    //     in original source where the call happened
91    //   - bindings: one entry per variable in the referenced original scope,
92    //     telling the debugger what JS expression to evaluate for each variable
93    //   - is_stack_frame: true if this range should appear in synthetic stacks
94    //   - is_hidden: true if the debugger should skip over this range entirely
95
96    let inlined_range = GeneratedRange {
97        start: Position { line: 1, column: 0 },
98        end: Position {
99            line: 3,
100            column: 22,
101        },
102        is_stack_frame: true,
103        is_hidden: false,
104        // definition=1 points to the `add` function scope (pre-order index 1)
105        definition: Some(1),
106        // The call site is where `add(10, 32)` was called in the original source.
107        // The debugger uses this to reconstruct a synthetic call stack:
108        //   add @ math.ts:2:2  (current position in the inlined body)
109        //   <module> @ math.ts:5:14  (the call site)
110        call_site: Some(CallSite {
111            source_index: 0,
112            line: 5,
113            column: 14,
114        }),
115        // Bindings map the original scope's variables to generated expressions.
116        // The `add` scope has variables ["a", "b"] (in that order), so:
117        //   bindings[0] = Expression("_a")  → original `a` is `_a` in generated code
118        //   bindings[1] = Expression("_b")  → original `b` is `_b` in generated code
119        bindings: vec![
120            Binding::Expression("_a".to_string()),
121            Binding::Expression("_b".to_string()),
122        ],
123        children: vec![],
124    };
125
126    let wrapper_range = GeneratedRange {
127        start: Position { line: 0, column: 0 },
128        end: Position { line: 4, column: 1 },
129        is_stack_frame: false,
130        is_hidden: false,
131        // definition=0 points to the module scope (pre-order index 0)
132        definition: Some(0),
133        call_site: None,
134        // The module scope has variables ["result"], and in the generated code
135        // the variable keeps its name, so we bind it to "result".
136        bindings: vec![Binding::Expression("result".to_string())],
137        children: vec![inlined_range],
138    };
139
140    // -----------------------------------------------------------------------
141    // 3. Assemble ScopeInfo and encode
142    // -----------------------------------------------------------------------
143    //
144    // ScopeInfo combines original scope trees (one per source file) with the
145    // generated ranges. The `scopes` vec is indexed by source index — None
146    // means no scope info for that source file.
147
148    let scope_info = ScopeInfo {
149        scopes: vec![Some(module_scope)],
150        ranges: vec![wrapper_range],
151    };
152
153    // Encoding produces a compact VLQ string (stored in the source map's
154    // "scopes" field) and populates the names array with any new name strings.
155    let mut names: Vec<String> = vec![];
156    let encoded = encode_scopes(&scope_info, &mut names);
157
158    println!("=== ECMA-426 Scopes Roundtrip ===\n");
159    println!("Encoded scopes: {encoded:?}");
160    println!("Names array:    {names:?}\n");
161
162    assert!(!encoded.is_empty(), "encoded string must not be empty");
163    assert!(
164        !names.is_empty(),
165        "names array must contain scope/variable names"
166    );
167
168    // -----------------------------------------------------------------------
169    // 4. Decode back and verify roundtrip
170    // -----------------------------------------------------------------------
171    //
172    // decode_scopes takes the encoded string, the names array, and the number
173    // of source files (so it knows how many original scope trees to expect).
174
175    let decoded = decode_scopes(&encoded, &names, 1).expect("decoding must succeed");
176
177    // Verify the original scope tree roundtrips correctly
178    assert_eq!(
179        decoded.scopes.len(),
180        1,
181        "must have exactly one source file's scopes"
182    );
183
184    let root_scope = decoded.scopes[0]
185        .as_ref()
186        .expect("source 0 must have scope info");
187
188    assert_eq!(root_scope.kind.as_deref(), Some("module"));
189    assert!(
190        !root_scope.is_stack_frame,
191        "module scope is not a stack frame"
192    );
193    assert_eq!(root_scope.variables, vec!["result"]);
194    assert_eq!(root_scope.start, Position { line: 0, column: 0 });
195    assert_eq!(
196        root_scope.end,
197        Position {
198            line: 5,
199            column: 27
200        }
201    );
202
203    println!("Original scope tree (source 0):");
204    println!(
205        "  Root: kind={:?}, variables={:?}",
206        root_scope.kind, root_scope.variables
207    );
208
209    assert_eq!(root_scope.children.len(), 1, "module has one child scope");
210
211    let func_scope = &root_scope.children[0];
212    assert_eq!(func_scope.name.as_deref(), Some("add"));
213    assert_eq!(func_scope.kind.as_deref(), Some("function"));
214    assert!(func_scope.is_stack_frame, "function scope is a stack frame");
215    assert_eq!(func_scope.variables, vec!["a", "b"]);
216    assert_eq!(func_scope.start, Position { line: 1, column: 0 });
217    assert_eq!(func_scope.end, Position { line: 3, column: 1 });
218
219    println!(
220        "  Child: name={:?}, kind={:?}, variables={:?}",
221        func_scope.name, func_scope.kind, func_scope.variables
222    );
223
224    // Verify the generated ranges roundtrip correctly
225    assert_eq!(
226        decoded.ranges.len(),
227        1,
228        "must have one top-level generated range"
229    );
230
231    let wrapper = &decoded.ranges[0];
232    assert_eq!(wrapper.definition, Some(0));
233    assert!(!wrapper.is_stack_frame);
234    assert!(!wrapper.is_hidden);
235    assert!(wrapper.call_site.is_none());
236    assert_eq!(
237        wrapper.bindings,
238        vec![Binding::Expression("result".to_string())]
239    );
240
241    println!("\nGenerated ranges:");
242    println!(
243        "  Wrapper: lines {}-{}, definition={:?}, bindings={:?}",
244        wrapper.start.line, wrapper.end.line, wrapper.definition, wrapper.bindings
245    );
246
247    assert_eq!(wrapper.children.len(), 1, "wrapper has one child range");
248
249    let inlined = &wrapper.children[0];
250    assert_eq!(inlined.definition, Some(1));
251    assert!(inlined.is_stack_frame, "inlined range is a stack frame");
252    assert!(!inlined.is_hidden);
253    assert_eq!(
254        inlined.call_site,
255        Some(CallSite {
256            source_index: 0,
257            line: 5,
258            column: 14,
259        })
260    );
261    assert_eq!(
262        inlined.bindings,
263        vec![
264            Binding::Expression("_a".to_string()),
265            Binding::Expression("_b".to_string()),
266        ]
267    );
268
269    println!(
270        "  Inlined: lines {}-{}, definition={:?}, call_site={:?}, bindings={:?}",
271        inlined.start.line,
272        inlined.end.line,
273        inlined.definition,
274        inlined.call_site,
275        inlined.bindings
276    );
277
278    // Full structural equality check
279    assert_eq!(
280        decoded, scope_info,
281        "decoded scope info must match the original"
282    );
283
284    println!("\nRoundtrip verified: decoded structure matches original.\n");
285
286    // -----------------------------------------------------------------------
287    // 5. Look up original scopes by definition index
288    // -----------------------------------------------------------------------
289    //
290    // A debugger hits a breakpoint in generated code and finds a generated
291    // range with definition=1. It needs to find the corresponding original
292    // scope to know the function name, parameter names, etc.
293
294    println!("--- Definition index lookups ---\n");
295
296    let scope_0 = decoded
297        .original_scope_for_definition(0)
298        .expect("definition 0 must exist");
299    assert_eq!(scope_0.kind.as_deref(), Some("module"));
300    println!(
301        "  definition 0: kind={:?}, name={:?}",
302        scope_0.kind, scope_0.name
303    );
304
305    let scope_1 = decoded
306        .original_scope_for_definition(1)
307        .expect("definition 1 must exist");
308    assert_eq!(scope_1.name.as_deref(), Some("add"));
309    assert_eq!(scope_1.variables, vec!["a", "b"]);
310    println!(
311        "  definition 1: kind={:?}, name={:?}",
312        scope_1.kind, scope_1.name
313    );
314
315    // Out-of-bounds definition index returns None
316    assert!(
317        decoded.original_scope_for_definition(99).is_none(),
318        "non-existent definition must return None"
319    );
320    println!("  definition 99: None (out of bounds)");
321
322    // -----------------------------------------------------------------------
323    // 6. Demonstrate the Unavailable binding variant
324    // -----------------------------------------------------------------------
325    //
326    // Sometimes a variable is optimized out entirely. The debugger should
327    // show it as "unavailable" rather than silently omitting it.
328
329    println!("\n--- Unavailable binding ---\n");
330
331    let optimized_info = ScopeInfo {
332        scopes: vec![Some(OriginalScope {
333            start: Position { line: 0, column: 0 },
334            end: Position { line: 3, column: 1 },
335            name: Some("compute".to_string()),
336            kind: Some("function".to_string()),
337            is_stack_frame: true,
338            variables: vec!["x".to_string(), "y".to_string()],
339            children: vec![],
340        })],
341        ranges: vec![GeneratedRange {
342            start: Position { line: 0, column: 0 },
343            end: Position { line: 1, column: 0 },
344            is_stack_frame: true,
345            is_hidden: false,
346            definition: Some(0),
347            call_site: None,
348            // x is available as "_x", but y was optimized out
349            bindings: vec![Binding::Expression("_x".to_string()), Binding::Unavailable],
350            children: vec![],
351        }],
352    };
353
354    let mut opt_names: Vec<String> = vec![];
355    let opt_encoded = encode_scopes(&optimized_info, &mut opt_names);
356    let opt_decoded = decode_scopes(&opt_encoded, &opt_names, 1).expect("decoding must succeed");
357
358    assert_eq!(opt_decoded, optimized_info);
359    println!("  Bindings: {:?}", opt_decoded.ranges[0].bindings);
360    println!("  Variable 'x' -> Expression(\"_x\"), variable 'y' -> Unavailable");
361
362    // -----------------------------------------------------------------------
363    // 7. Error handling for invalid input
364    // -----------------------------------------------------------------------
365    //
366    // decode_scopes returns ScopesError for malformed input. This is useful
367    // for tools that validate source maps.
368
369    println!("\n--- Error handling ---\n");
370
371    // Empty encoded string is valid (no scopes, no ranges)
372    let empty_result = decode_scopes("", &[], 0);
373    assert!(empty_result.is_ok(), "empty input is valid");
374    println!("  Empty input: ok (no scopes, no ranges)");
375
376    // Invalid VLQ data: 'z' is not a valid base64 character for VLQ
377    let bad_vlq = decode_scopes("!!!", &[], 1);
378    assert!(bad_vlq.is_err(), "invalid VLQ must produce an error");
379    println!("  Invalid VLQ (\"!!!\"): {}", bad_vlq.unwrap_err());
380
381    println!("\nAll assertions passed.");
382}

Trait Implementations§

Source§

impl Clone for ScopeInfo

Source§

fn clone(&self) -> ScopeInfo

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ScopeInfo

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for ScopeInfo

Source§

fn eq(&self, other: &ScopeInfo) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Eq for ScopeInfo

Source§

impl StructuralPartialEq for ScopeInfo

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.