scopes.js (7373B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 // This file contains utility functions which supports the structure & display of 6 // scopes information in Scopes panel. 7 8 import * as objectInspector from "resource://devtools/client/shared/components/object-inspector/index.js"; 9 import { simplifyDisplayName } from "../pause/frames/index"; 10 11 const { 12 utils: { 13 node: { NODE_TYPES }, 14 }, 15 } = objectInspector; 16 17 // The heading that should be displayed for the scope 18 function _getScopeTitle(type, scope) { 19 if (type === "block" && scope.block && scope.block.displayName) { 20 return scope.block.displayName; 21 } 22 23 if (type === "function" && scope.function) { 24 return scope.function.displayName 25 ? simplifyDisplayName(scope.function.displayName) 26 : L10N.getStr("anonymousFunction"); 27 } 28 return L10N.getStr("scopes.block"); 29 } 30 31 function _getThisVariable(this_, path) { 32 if (!this_) { 33 return null; 34 } 35 36 return { 37 name: "<this>", 38 path: `${path}/<this>`, 39 contents: { value: this_ }, 40 }; 41 } 42 43 /** 44 * Builds a tree of nodes representing all the variables and arguments 45 * for the bindings from a scope. 46 * 47 * Each binding => { variables: Array, arguments: Array } 48 * Each binding argument => [name: string, contents: BindingContents] 49 * 50 * @param {Array} bindings 51 * @param {string} parentName 52 * @returns 53 */ 54 function _getBindingVariables(bindings, parentName) { 55 if (!bindings) { 56 return []; 57 } 58 59 const nodes = []; 60 const addNode = (name, contents) => 61 nodes.push({ name, contents, path: `${parentName}/${name}` }); 62 63 for (const arg of bindings.arguments) { 64 // `arg` is an object which only has a single property whose name is the name of the 65 // argument. So here we can directly pick the first (and only) entry of `arg` 66 const [name, contents] = Object.entries(arg)[0]; 67 addNode(name, contents); 68 } 69 70 for (const name in bindings.variables) { 71 addNode(name, bindings.variables[name]); 72 } 73 74 return nodes; 75 } 76 77 /** 78 * This generates the scope item for rendering in the scopes panel. 79 * 80 * @param {*} scope 81 * @param {*} selectedFrame 82 * @param {*} frameScopes 83 * @param {*} why 84 * @param {*} scopeIndex 85 * @returns 86 */ 87 function _getScopeItem(scope, selectedFrame, frameScopes, why, scopeIndex) { 88 const { type, actor } = scope; 89 90 const isLocalScope = scope.actor === frameScopes.actor; 91 92 const key = `${actor}-${scopeIndex}`; 93 if (type === "function" || type === "block") { 94 const { bindings } = scope; 95 96 let vars = _getBindingVariables(bindings, key); 97 98 // show exception, return, and this variables in innermost scope 99 if (isLocalScope) { 100 vars = vars.concat(_getFrameExceptionOrReturnedValueVariables(why, key)); 101 102 let thisDesc_ = selectedFrame.this; 103 104 if (bindings && "this" in bindings) { 105 // The presence of "this" means we're rendering a "this" binding 106 // generated from mapScopes and this can override the binding 107 // provided by the current frame. 108 thisDesc_ = bindings.this ? bindings.this.value : null; 109 } 110 111 const this_ = _getThisVariable(thisDesc_, key); 112 113 if (this_) { 114 vars.push(this_); 115 } 116 } 117 118 if (vars?.length) { 119 const title = _getScopeTitle(type, scope) || ""; 120 vars.sort((a, b) => a.name.localeCompare(b.name)); 121 return { 122 name: title, 123 path: key, 124 contents: vars, 125 type: NODE_TYPES.BLOCK, 126 }; 127 } 128 } else if (type === "object" && scope.object) { 129 let value = scope.object; 130 // If this is the global window scope, mark it as such so that it will 131 // preview Window: Global instead of Window: Window 132 if (value.class === "Window") { 133 value = { ...value, displayClass: "Global" }; 134 } 135 return { 136 name: scope.object.class, 137 path: key, 138 contents: { value }, 139 }; 140 } 141 142 return null; 143 } 144 /** 145 * Merge the scope bindings for lexical scopes and its parent function body scopes 146 * Note: block scopes are not merged. See browser_dbg-merge-scopes.js for test examples 147 * to better understand the scenario, 148 * 149 * @param {*} scope 150 * @param {*} parentScope 151 * @param {*} item 152 * @param {*} parentItem 153 * @returns 154 */ 155 export function _mergeLexicalScopesBindings( 156 scope, 157 parentScope, 158 item, 159 parentItem 160 ) { 161 if (scope.scopeKind == "function lexical" && parentScope.type == "function") { 162 const contents = item.contents.concat(parentItem.contents); 163 contents.sort((a, b) => a.name.localeCompare(b.name)); 164 165 return { 166 name: parentItem.name, 167 path: parentItem.path, 168 contents, 169 type: NODE_TYPES.BLOCK, 170 }; 171 } 172 return null; 173 } 174 175 /** 176 * Returns a string path for an scope item which can be used 177 * in different pauses for a thread. 178 * 179 * @param {object} item 180 * @returns 181 */ 182 183 export function getScopeItemPath(item) { 184 // Calling toString() on item.path allows symbols to be handled. 185 return item.path.toString(); 186 } 187 188 // Generate variables when the function throws an exception or returned a value. 189 function _getFrameExceptionOrReturnedValueVariables(why, path) { 190 const vars = []; 191 192 if (why && why.frameFinished) { 193 const { frameFinished } = why; 194 195 // Always display a `throw` property if present, even if it is falsy. 196 if (Object.prototype.hasOwnProperty.call(frameFinished, "throw")) { 197 vars.push({ 198 name: "<exception>", 199 path: `${path}/<exception>`, 200 contents: { value: frameFinished.throw }, 201 }); 202 } 203 204 if (Object.prototype.hasOwnProperty.call(frameFinished, "return")) { 205 const returned = frameFinished.return; 206 207 // Do not display undefined. Do display falsy values like 0 and false. The 208 // protocol grip for undefined is a JSON object: { type: "undefined" }. 209 if (typeof returned !== "object" || returned.type !== "undefined") { 210 vars.push({ 211 name: "<return>", 212 path: `${path}/<return>`, 213 contents: { value: returned }, 214 }); 215 } 216 } 217 } 218 219 return vars; 220 } 221 222 /** 223 * Generates the scope items (for scopes related to selected frame) to be rendered in the scope panel 224 * 225 * @param {*} why 226 * @param {*} selectedFrame 227 * @param {*} frameScopes 228 * @returns 229 */ 230 export function getScopesItemsForSelectedFrame( 231 why, 232 selectedFrame, 233 frameScopes 234 ) { 235 if (!why || !selectedFrame) { 236 return null; 237 } 238 239 if (!frameScopes) { 240 return null; 241 } 242 243 const scopes = []; 244 245 let currentScope = frameScopes; 246 let currentScopeIndex = 1; 247 248 let prevScope = null, 249 prevScopeItem = null; 250 251 while (currentScope) { 252 let currentScopeItem = _getScopeItem( 253 currentScope, 254 selectedFrame, 255 frameScopes, 256 why, 257 currentScopeIndex 258 ); 259 260 if (currentScopeItem) { 261 const mergedItem = 262 prevScope && prevScopeItem 263 ? _mergeLexicalScopesBindings( 264 prevScope, 265 currentScope, 266 prevScopeItem, 267 currentScopeItem 268 ) 269 : null; 270 if (mergedItem) { 271 currentScopeItem = mergedItem; 272 scopes.pop(); 273 } 274 scopes.push(currentScopeItem); 275 } 276 277 prevScope = currentScope; 278 prevScopeItem = currentScopeItem; 279 currentScopeIndex++; 280 currentScope = currentScope.parent; 281 } 282 283 return scopes; 284 }