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

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.