summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/utils/pause/scopes
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/utils/pause/scopes')
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/getScope.js131
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/getVariables.js48
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/index.js63
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/moz.build13
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/tests/getFramePopVariables.spec.js117
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/tests/scopes.spec.js139
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/types.js14
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/utils.js60
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();
+}