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
impl ScopeInfo
Sourcepub fn original_scope_for_definition(
&self,
definition: u32,
) -> Option<&OriginalScope>
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§
impl Eq for ScopeInfo
impl StructuralPartialEq for ScopeInfo
Auto Trait Implementations§
impl Freeze for ScopeInfo
impl RefUnwindSafe for ScopeInfo
impl Send for ScopeInfo
impl Sync for ScopeInfo
impl Unpin for ScopeInfo
impl UnsafeUnpin for ScopeInfo
impl UnwindSafe for ScopeInfo
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more