diff options
Diffstat (limited to 'devtools/client/debugger/src/utils/pause/scopes.js')
-rw-r--r-- | devtools/client/debugger/src/utils/pause/scopes.js | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/utils/pause/scopes.js b/devtools/client/debugger/src/utils/pause/scopes.js new file mode 100644 index 0000000000..bdb53ba493 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes.js @@ -0,0 +1,283 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +// This file contains utility functions which supports the structure & display of +// scopes information in Scopes panel. + +import { objectInspector } from "devtools/client/shared/components/reps/index"; +import { simplifyDisplayName } from "../pause/frames/index"; + +const { + utils: { + node: { NODE_TYPES }, + }, +} = objectInspector; + +// The heading that should be displayed for the scope +function _getScopeTitle(type, scope) { + if (type === "block" && scope.block && scope.block.displayName) { + return scope.block.displayName; + } + + if (type === "function" && scope.function) { + return scope.function.displayName + ? simplifyDisplayName(scope.function.displayName) + : L10N.getStr("anonymousFunction"); + } + return L10N.getStr("scopes.block"); +} + +function _getThisVariable(this_, path) { + if (!this_) { + return null; + } + + return { + name: "<this>", + path: `${path}/<this>`, + contents: { value: this_ }, + }; +} + +/** + * Builds a tree of nodes representing all the variables and arguments + * for the bindings from a scope. + * + * Each binding => { variables: Array, arguments: Array } + * Each binding argument => [name: string, contents: BindingContents] + * + * @param {Array} bindings + * @param {String} parentName + * @returns + */ +function _getBindingVariables(bindings, parentName) { + if (!bindings) { + return []; + } + + const nodes = []; + const addNode = (name, contents) => + nodes.push({ name, contents, path: `${parentName}/${name}` }); + + for (const arg of bindings.arguments) { + // `arg` is an object which only has a single property whose name is the name of the + // argument. So here we can directly pick the first (and only) entry of `arg` + const [name, contents] = Object.entries(arg)[0]; + addNode(name, contents); + } + + for (const name in bindings.variables) { + addNode(name, bindings.variables[name]); + } + + return nodes; +} + +/** + * This generates the scope item for rendering in the scopes panel. + * + * @param {*} scope + * @param {*} selectedFrame + * @param {*} frameScopes + * @param {*} why + * @param {*} scopeIndex + * @returns + */ +function _getScopeItem(scope, selectedFrame, frameScopes, why, scopeIndex) { + const { type, actor } = scope; + + const isLocalScope = scope.actor === frameScopes.actor; + + const key = `${actor}-${scopeIndex}`; + if (type === "function" || type === "block") { + const { bindings } = scope; + + let vars = _getBindingVariables(bindings, key); + + // show exception, return, and this variables in innermost scope + if (isLocalScope) { + vars = vars.concat(_getFrameExceptionOrReturnedValueVariables(why, key)); + + let thisDesc_ = selectedFrame.this; + + if (bindings && "this" in bindings) { + // The presence of "this" means we're rendering a "this" binding + // generated from mapScopes and this can override the binding + // provided by the current frame. + thisDesc_ = bindings.this ? bindings.this.value : null; + } + + const this_ = _getThisVariable(thisDesc_, key); + + if (this_) { + vars.push(this_); + } + } + + if (vars?.length) { + const title = _getScopeTitle(type, scope) || ""; + vars.sort((a, b) => a.name.localeCompare(b.name)); + return { + name: title, + path: key, + contents: vars, + type: NODE_TYPES.BLOCK, + }; + } + } else if (type === "object" && scope.object) { + let value = scope.object; + // If this is the global window scope, mark it as such so that it will + // preview Window: Global instead of Window: Window + if (value.class === "Window") { + value = { ...value, displayClass: "Global" }; + } + return { + name: scope.object.class, + path: key, + contents: { value }, + }; + } + + return null; +} +/** + * Merge the scope bindings for lexical scopes and its parent function body scopes + * Note: block scopes are not merged. See browser_dbg-merge-scopes.js for test examples + * to better understand the scenario, + * + * @param {*} scope + * @param {*} parentScope + * @param {*} item + * @param {*} parentItem + * @returns + */ +export function _mergeLexicalScopesBindings( + scope, + parentScope, + item, + parentItem +) { + if (scope.scopeKind == "function lexical" && parentScope.type == "function") { + const contents = item.contents.concat(parentItem.contents); + contents.sort((a, b) => a.name.localeCompare(b.name)); + + return { + name: parentItem.name, + path: parentItem.path, + contents, + type: NODE_TYPES.BLOCK, + }; + } + return null; +} + +/** + * Returns a string path for an scope item which can be used + * in different pauses for a thread. + * + * @param {Object} item + * @returns + */ + +export function getScopeItemPath(item) { + // Calling toString() on item.path allows symbols to be handled. + return item.path.toString(); +} + +// Generate variables when the function throws an exception or returned a value. +function _getFrameExceptionOrReturnedValueVariables(why, path) { + const vars = []; + + if (why && why.frameFinished) { + const { frameFinished } = why; + + // Always display a `throw` property if present, even if it is falsy. + if (Object.prototype.hasOwnProperty.call(frameFinished, "throw")) { + vars.push({ + name: "<exception>", + path: `${path}/<exception>`, + contents: { value: frameFinished.throw }, + }); + } + + if (Object.prototype.hasOwnProperty.call(frameFinished, "return")) { + const returned = frameFinished.return; + + // Do not display undefined. Do display falsy values like 0 and false. The + // protocol grip for undefined is a JSON object: { type: "undefined" }. + if (typeof returned !== "object" || returned.type !== "undefined") { + vars.push({ + name: "<return>", + path: `${path}/<return>`, + contents: { value: returned }, + }); + } + } + } + + return vars; +} + +/** + * Generates the scope items (for scopes related to selected frame) to be rendered in the scope panel + * @param {*} why + * @param {*} selectedFrame + * @param {*} frameScopes + * @returns + */ +export function getScopesItemsForSelectedFrame( + why, + selectedFrame, + frameScopes +) { + if (!why || !selectedFrame) { + return null; + } + + if (!frameScopes) { + return null; + } + + const scopes = []; + + let currentScope = frameScopes; + let currentScopeIndex = 1; + + let prevScope = null, + prevScopeItem = null; + + while (currentScope) { + let currentScopeItem = _getScopeItem( + currentScope, + selectedFrame, + frameScopes, + why, + currentScopeIndex + ); + + if (currentScopeItem) { + const mergedItem = + prevScope && prevScopeItem + ? _mergeLexicalScopesBindings( + prevScope, + currentScope, + prevScopeItem, + currentScopeItem + ) + : null; + if (mergedItem) { + currentScopeItem = mergedItem; + scopes.pop(); + } + scopes.push(currentScopeItem); + } + + prevScope = currentScope; + prevScopeItem = currentScopeItem; + currentScopeIndex++; + currentScope = currentScope.parent; + } + + return scopes; +} |