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.js101
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/getVariables.js32
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/index.js48
-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.js114
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/tests/scopes.spec.js134
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes/utils.js55
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();
+}