diff options
Diffstat (limited to 'devtools/client/debugger/src/utils/pause/scopes')
8 files changed, 585 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..fae8a6bdf6 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/getScope.js @@ -0,0 +1,131 @@ +/* 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/>. */ + +// @flow +// $FlowIgnore +import { objectInspector } from "devtools/client/shared/components/reps/index"; +import { getBindingVariables } from "./getVariables"; +import { getFramePopVariables, getThisVariable } from "./utils"; +import { simplifyDisplayName } from "../../pause/frames"; + +import type { Frame, Why, Scope } from "../../../types"; + +import type { NamedValue } from "./types"; + +export type RenderableScope = { + type: $ElementType<Scope, "type">, + scopeKind: $ElementType<Scope, "scopeKind">, + actor: $ElementType<Scope, "actor">, + bindings: $ElementType<Scope, "bindings">, + parent: ?RenderableScope, + object?: ?Object, + function?: ?{ + displayName: string, + }, + block?: ?{ + displayName: string, + }, +}; + +const { + utils: { + node: { NODE_TYPES }, + }, +} = objectInspector; + +function getScopeTitle(type, scope: RenderableScope): string | void { + 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: RenderableScope, + selectedFrame: Frame, + frameScopes: RenderableScope, + why: Why, + scopeIndex: number +): ?NamedValue { + 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: RenderableScope, + parentScope: RenderableScope, + item: NamedValue, + parentItem: NamedValue +): NamedValue | void { + if (scope.scopeKind == "function lexical" && parentScope.type == "function") { + const contents = (item.contents: any).concat(parentItem.contents); + contents.sort((a, b) => a.name.localeCompare(b.name)); + + return { + name: parentItem.name, + path: parentItem.path, + contents, + type: NODE_TYPES.BLOCK, + }; + } +} 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..9fd2402a52 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/getVariables.js @@ -0,0 +1,48 @@ +/* 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/>. */ + +// @flow +import { toPairs } from "lodash"; + +import type { NamedValue } from "./types"; +import type { BindingContents, ScopeBindings } from "../../../types"; + +// VarAndBindingsPair actually is [name: string, contents: BindingContents] +type VarAndBindingsPair = [string, any]; +type VarAndBindingsPairs = Array<VarAndBindingsPair>; + +// Scope's bindings field which holds variables and arguments +type ScopeBindingsWrapper = { + variables: ScopeBindings, + arguments: BindingContents[], +}; + +// Create the tree nodes representing all the variables and arguments +// for the bindings from a scope. +export function getBindingVariables( + bindings: ?ScopeBindingsWrapper, + parentName: string +): NamedValue[] { + if (!bindings) { + return []; + } + + const args: VarAndBindingsPairs = bindings.arguments.map( + arg => toPairs(arg)[0] + ); + + const variables: VarAndBindingsPairs = toPairs(bindings.variables); + + return args.concat(variables).map(binding => { + const name = binding[0]; + const contents = binding[1]; + return { + name, + path: `${parentName}/${name}`, + contents, + }; + }); +} 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..2b5ae9efe8 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/index.js @@ -0,0 +1,63 @@ +/* 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/>. */ + +// @flow + +import { getScope, mergeScopes, type RenderableScope } from "./getScope"; + +import type { Frame, Why, BindingContents } from "../../../types"; + +export type NamedValue = { + name: string, + generatedName?: string, + path: string, + contents: BindingContents | NamedValue[], +}; + +export function getScopes( + why: ?Why, + selectedFrame: Frame, + frameScopes: ?RenderableScope +): ?(NamedValue[]) { + 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..34d5631407 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/tests/getFramePopVariables.spec.js @@ -0,0 +1,117 @@ +/* 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/>. */ + +// @flow + +import { getFramePopVariables } from "../utils"; +import type { NamedValue } from "../types"; + +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: NamedValue) { + return (v.contents: any).value; +} + +function getContentsClass(v: NamedValue) { + 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..24fedefe45 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/tests/scopes.spec.js @@ -0,0 +1,139 @@ +/* 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/>. */ + +// @flow + +import { getScopes } from ".."; +import { + makeMockFrame, + makeMockScope, + makeWhyNormal, + makeWhyThrow, + mockScopeAddVariable, +} from "../../../test-mockup"; + +import type { Scope } from "../../../../types"; +import type { RenderableScope } from "../getScope"; + +function convertScope(scope: Scope): RenderableScope { + return (scope: any); +} + +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/types.js b/devtools/client/debugger/src/utils/pause/scopes/types.js new file mode 100644 index 0000000000..4c44b4c530 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/types.js @@ -0,0 +1,14 @@ +/* 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/>. */ + +// @flow + +import type { BindingContents } from "../../../types"; + +export type NamedValue = { + name: string, + generatedName?: string, + path: string, + contents: BindingContents | NamedValue[], +}; 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..b9f2924ced --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/scopes/utils.js @@ -0,0 +1,60 @@ +/* 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/>. */ + +// @flow + +import type { Why } from "../../../types"; +import type { NamedValue } from "./types"; + +export function getFramePopVariables(why: Why, path: string): NamedValue[] { + const vars: Array<NamedValue> = []; + + 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_: any, path: string): ?NamedValue { + 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: Object): string { + // Calling toString() on item.path allows symbols to be handled. + return item.path.toString(); +} |