diff options
Diffstat (limited to 'devtools/client/debugger/src/utils/pause/scopes')
7 files changed, 497 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/utils/pause/scopes/getScope.js b/devtools/client/debugger/src/utils/pause/scopes/getScope.js new file mode 100644 index 0000000000..549e387d51 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/getScope.js @@ -0,0 +1,101 @@ +/* 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/>. */ + +import { objectInspector } from "devtools/client/shared/components/reps/index"; +import { getBindingVariables } from "./getVariables"; +import { getFramePopVariables, getThisVariable } from "./utils"; +import { simplifyDisplayName } from "../../pause/frames"; + +const { + utils: { + node: { NODE_TYPES }, + }, +} = objectInspector; + +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"); +} + +export function getScope(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(getFramePopVariables(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 = { ...scope.object, displayClass: "Global" }; + } + return { + name: scope.object.class, + path: key, + contents: { value }, + }; + } + + return null; +} + +export function mergeScopes(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; +} diff --git a/devtools/client/debugger/src/utils/pause/scopes/getVariables.js b/devtools/client/debugger/src/utils/pause/scopes/getVariables.js new file mode 100644 index 0000000000..3346a99cdb --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/getVariables.js @@ -0,0 +1,32 @@ +/* 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/>. */ + +// VarAndBindingsPair actually is [name: string, contents: BindingContents] + +// Scope's bindings field which holds variables and arguments + +// Create the tree nodes representing all the variables and arguments +// for the bindings from a scope. +export 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; +} diff --git a/devtools/client/debugger/src/utils/pause/scopes/index.js b/devtools/client/debugger/src/utils/pause/scopes/index.js new file mode 100644 index 0000000000..2f24a41640 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/index.js @@ -0,0 +1,48 @@ +/* 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/>. */ + +import { getScope, mergeScopes } from "./getScope"; + +export function getScopes(why, selectedFrame, frameScopes) { + if (!why || !selectedFrame) { + return null; + } + + if (!frameScopes) { + return null; + } + + const scopes = []; + + let scope = frameScopes; + let scopeIndex = 1; + let prev = null, + prevItem = null; + + while (scope) { + let scopeItem = getScope( + scope, + selectedFrame, + frameScopes, + why, + scopeIndex + ); + + if (scopeItem) { + const mergedItem = + prev && prevItem ? mergeScopes(prev, scope, prevItem, scopeItem) : null; + if (mergedItem) { + scopeItem = mergedItem; + scopes.pop(); + } + scopes.push(scopeItem); + } + prev = scope; + prevItem = scopeItem; + scopeIndex++; + scope = scope.parent; + } + + return scopes; +} diff --git a/devtools/client/debugger/src/utils/pause/scopes/moz.build b/devtools/client/debugger/src/utils/pause/scopes/moz.build new file mode 100644 index 0000000000..059d187e3d --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/moz.build @@ -0,0 +1,13 @@ +# vim: set filetype=python: +# 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/. + +DIRS += [] + +CompiledModules( + "getScope.js", + "getVariables.js", + "index.js", + "utils.js", +) diff --git a/devtools/client/debugger/src/utils/pause/scopes/tests/getFramePopVariables.spec.js b/devtools/client/debugger/src/utils/pause/scopes/tests/getFramePopVariables.spec.js new file mode 100644 index 0000000000..cf05c71345 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/tests/getFramePopVariables.spec.js @@ -0,0 +1,114 @@ +/* eslint max-nested-callbacks: ["error", 4] */ +/* 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/>. */ + +import { getFramePopVariables } from "../utils"; + +const errorGrip = { + type: "object", + actor: "server2.conn66.child1/obj243", + class: "Error", + extensible: true, + frozen: false, + sealed: false, + ownPropertyLength: 4, + preview: { + kind: "Error", + name: "Error", + message: "blah", + stack: + "onclick@http://localhost:8000/examples/doc-return-values.html:1:18\n", + fileName: "http://localhost:8000/examples/doc-return-values.html", + lineNumber: 1, + columnNumber: 18, + }, +}; + +function returnWhy(grip) { + return { + type: "resumeLimit", + frameFinished: { + return: grip, + }, + }; +} + +function throwWhy(grip) { + return { + type: "resumeLimit", + frameFinished: { + throw: grip, + }, + }; +} + +function getContentsValue(v) { + return v.contents.value; +} + +function getContentsClass(v) { + const value = getContentsValue(v); + return value ? value.class || undefined : ""; +} + +describe("pause - scopes", () => { + describe("getFramePopVariables", () => { + describe("falsey values", () => { + // NOTE: null and undefined are treated like objects and given a type + const falsey = { false: false, 0: 0, null: { type: "null" } }; + for (const test in falsey) { + const value = falsey[test]; + it(`shows ${test} returns`, () => { + const why = returnWhy(value); + const vars = getFramePopVariables(why, ""); + expect(vars[0].name).toEqual("<return>"); + expect(vars[0].name).toEqual("<return>"); + expect(getContentsValue(vars[0])).toEqual(value); + }); + + it(`shows ${test} throws`, () => { + const why = throwWhy(value); + const vars = getFramePopVariables(why, ""); + expect(vars[0].name).toEqual("<exception>"); + expect(vars[0].name).toEqual("<exception>"); + expect(getContentsValue(vars[0])).toEqual(value); + }); + } + }); + + describe("Error / Objects", () => { + it("shows Error returns", () => { + const why = returnWhy(errorGrip); + const vars = getFramePopVariables(why, ""); + expect(vars[0].name).toEqual("<return>"); + expect(vars[0].name).toEqual("<return>"); + expect(getContentsClass(vars[0])).toEqual("Error"); + }); + + it("shows error throws", () => { + const why = throwWhy(errorGrip); + const vars = getFramePopVariables(why, ""); + expect(vars[0].name).toEqual("<exception>"); + expect(vars[0].name).toEqual("<exception>"); + expect(getContentsClass(vars[0])).toEqual("Error"); + }); + }); + + describe("undefined", () => { + it("does not show undefined returns", () => { + const why = returnWhy({ type: "undefined" }); + const vars = getFramePopVariables(why, ""); + expect(vars).toHaveLength(0); + }); + + it("shows undefined throws", () => { + const why = throwWhy({ type: "undefined" }); + const vars = getFramePopVariables(why, ""); + expect(vars[0].name).toEqual("<exception>"); + expect(vars[0].name).toEqual("<exception>"); + expect(getContentsValue(vars[0])).toEqual({ type: "undefined" }); + }); + }); + }); +}); diff --git a/devtools/client/debugger/src/utils/pause/scopes/tests/scopes.spec.js b/devtools/client/debugger/src/utils/pause/scopes/tests/scopes.spec.js new file mode 100644 index 0000000000..07a962dfab --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/tests/scopes.spec.js @@ -0,0 +1,134 @@ +/* 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/>. */ + +import { getScopes } from ".."; +import { + makeMockFrame, + makeMockScope, + makeWhyNormal, + makeWhyThrow, + mockScopeAddVariable, +} from "../../../test-mockup"; + +function convertScope(scope) { + return scope; +} + +describe("scopes", () => { + describe("getScopes", () => { + it("single scope", () => { + const pauseData = makeWhyNormal(); + const scope = makeMockScope("actor1"); + const selectedFrame = makeMockFrame(undefined, undefined, scope); + + if (!selectedFrame.scope) { + throw new Error("Frame must include scopes"); + } + + const frameScopes = convertScope(selectedFrame.scope); + const scopes = getScopes(pauseData, selectedFrame, frameScopes); + if (!scopes) { + throw new Error("missing scopes"); + } + expect(scopes[0].path).toEqual("actor1-1"); + expect(scopes[0].contents[0]).toEqual({ + name: "<this>", + path: "actor1-1/<this>", + contents: { value: {} }, + }); + }); + + it("second scope", () => { + const pauseData = makeWhyNormal(); + const scope0 = makeMockScope("actor2"); + const scope1 = makeMockScope("actor1", undefined, scope0); + const selectedFrame = makeMockFrame(undefined, undefined, scope1); + mockScopeAddVariable(scope0, "foo"); + + if (!selectedFrame.scope) { + throw new Error("Frame must include scopes"); + } + + const frameScopes = convertScope(selectedFrame.scope); + const scopes = getScopes(pauseData, selectedFrame, frameScopes); + if (!scopes) { + throw new Error("missing scopes"); + } + expect(scopes[1].path).toEqual("actor2-2"); + expect(scopes[1].contents[0]).toEqual({ + name: "foo", + path: "actor2-2/foo", + contents: { value: null }, + }); + }); + + it("returning scope", () => { + const why = makeWhyNormal("to sender"); + const scope = makeMockScope("actor1"); + const selectedFrame = makeMockFrame(undefined, undefined, scope); + + if (!selectedFrame.scope) { + throw new Error("Frame must include scopes"); + } + + const frameScopes = convertScope(selectedFrame.scope); + const scopes = getScopes(why, selectedFrame, frameScopes); + expect(scopes).toMatchObject([ + { + path: "actor1-1", + contents: [ + { + name: "<return>", + path: "actor1-1/<return>", + contents: { + value: "to sender", + }, + }, + { + name: "<this>", + path: "actor1-1/<this>", + contents: { + value: {}, + }, + }, + ], + }, + ]); + }); + + it("throwing scope", () => { + const why = makeWhyThrow("a party"); + const scope = makeMockScope("actor1"); + const selectedFrame = makeMockFrame(undefined, undefined, scope); + + if (!selectedFrame.scope) { + throw new Error("Frame must include scopes"); + } + + const frameScopes = convertScope(selectedFrame.scope); + const scopes = getScopes(why, selectedFrame, frameScopes); + expect(scopes).toMatchObject([ + { + path: "actor1-1", + contents: [ + { + name: "<exception>", + path: "actor1-1/<exception>", + contents: { + value: "a party", + }, + }, + { + name: "<this>", + path: "actor1-1/<this>", + contents: { + value: {}, + }, + }, + ], + }, + ]); + }); + }); +}); diff --git a/devtools/client/debugger/src/utils/pause/scopes/utils.js b/devtools/client/debugger/src/utils/pause/scopes/utils.js new file mode 100644 index 0000000000..16ebbf04a9 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/utils.js @@ -0,0 +1,55 @@ +/* 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/>. */ + +export function getFramePopVariables(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; +} + +export function getThisVariable(this_, path) { + if (!this_) { + return null; + } + + return { + name: "<this>", + path: `${path}/<this>`, + contents: { value: this_ }, + }; +} + +// Get a string path for an scope item which can be used in different pauses for +// a thread. +export function getScopeItemPath(item) { + // Calling toString() on item.path allows symbols to be handled. + return item.path.toString(); +} |