Bug 1274657 - When inspecting a proxy, show the [[ProxyHandler]] and [[ProxyTarget]] instead of executing traps. r=jlong
authorOriol <oriol-bugzilla@hotmail.com>
Sat, 23 Jul 2016 17:39:00 +0200
changeset 346681 2e03460fc9272cd1f8141d337c5854e8f0bf8fe6
parent 346680 e6c96d5612d3ae57296734645fdb5a2522dc961d
child 346682 b1fce35d1cc8e057d4540dd64d7433394993f9ef
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlong
bugs1274657
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1274657 - When inspecting a proxy, show the [[ProxyHandler]] and [[ProxyTarget]] instead of executing traps. r=jlong
devtools/client/debugger/test/mochitest/browser.ini
devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js
devtools/client/debugger/test/mochitest/doc_proxy.html
devtools/client/shared/widgets/VariablesViewController.jsm
devtools/client/webconsole/test/browser_webconsole_output_05.js
devtools/server/actors/object.js
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -96,16 +96,17 @@ support-files =
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_pretty-print-3.html
   doc_pretty-print-on-paused.html
   doc_promise-get-allocation-stack.html
   doc_promise-get-fulfillment-stack.html
   doc_promise-get-rejection-stack.html
   doc_promise.html
+  doc_proxy.html
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_scope-variable-4.html
   doc_script-eval.html
   doc_script-bookmarklet.html
@@ -513,16 +514,18 @@ skip-if = e10s && debug
 [browser_dbg_variables-view-03.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-04.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-05.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-06.js]
 skip-if = e10s && debug
+[browser_dbg_variables-view-07.js]
+skip-if = e10s && debug
 [browser_dbg_variables-view-accessibility.js]
 subsuite = clipboard
 skip-if = e10s && debug
 [browser_dbg_variables-view-data.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-edit-cancel.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-edit-click.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-07.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that proxy objects get their internal state added as pseudo properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_proxy.html";
+
+var test = Task.async(function* () {
+  let options = {
+    source: TAB_URL,
+    line: 1
+  };
+  var dbg = initDebugger(TAB_URL, options);
+  const [tab,, panel] = yield dbg;
+  const debuggerLineNumber = 34;
+  const scopes = waitForCaretAndScopes(panel, debuggerLineNumber);
+  callInTab(tab, "doPause");
+  yield scopes;
+
+  const variables = panel.panelWin.DebuggerView.Variables;
+  ok(variables, "Should get the variables view.");
+
+  const scope = [...variables][0];
+  ok(scope, "Should get the current function's scope.");
+
+  let proxy;
+  [...scope].forEach(function([name, value]) {
+    if(name === "proxy") proxy = value;
+  });
+  ok(proxy, "Should have found the proxy variable");
+
+  info("Expanding variable 'proxy'");
+  let expanded = once(variables, "fetched");
+  proxy.expand();
+  yield expanded;
+
+  let foundTarget = false;
+  let foundHandler = false;
+  for (let [property, data] of proxy) {
+    info("Expanding property '" + property + "'");
+    let expanded = once(variables, "fetched");
+    data.expand();
+    yield expanded;
+    if (property === "<target>") {
+      for(let [subprop, subdata] of data) if(subprop === "name") {
+        is(subdata.value, "target", "The value of '<target>' should be the [[ProxyTarget]]");
+        foundTarget = true;
+      }
+    } else {
+      is(property, "<handler>", "There shouldn't be properties other than <target> and <handler>");
+      for(let [subprop, subdata] of data) if(subprop === "name") {
+        is(subdata.value, "handler", "The value of '<handler>' should be the [[ProxyHandler]]");
+        foundHandler = true;
+      }
+    }
+  }
+  ok(foundTarget, "Should have found the '<target>' property containing the [[ProxyTarget]]");
+  ok(foundHandler, "Should have found the '<handler>' property containing the [[ProxyHandler]]");
+
+  debugger;
+
+  resumeDebuggerThenCloseAndFinish(panel);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_proxy.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger + Proxy test page</title>
+  </head>
+
+  <body>
+    <script>
+    window.target = {name: "target"};
+    window.handler = { /* Debugging a proxy shouldn't run any trap */
+      name: "handler",
+      getPrototypeOf() { throw new Error("proxy getPrototypeOf trap was called"); },
+      setPrototypeOf() { throw new Error("proxy setPrototypeOf trap was called"); },
+      isExtensible() { throw new Error("proxy isExtensible trap was called"); },
+      preventExtensions() { throw new Error("proxy preventExtensions trap was called"); },
+      getOwnPropertyDescriptor() { throw new Error("proxy getOwnPropertyDescriptor trap was called"); },
+      defineProperty() { throw new Error("proxy defineProperty trap was called"); },
+      has() { throw new Error("proxy has trap was called"); },
+      get() { throw new Error("proxy get trap was called"); },
+      set() { throw new Error("proxy set trap was called"); },
+      deleteProperty() { throw new Error("proxy deleteProperty trap was called"); },
+      ownKeys() { throw new Error("proxy ownKeys trap was called"); },
+      apply() { throw new Error("proxy apply trap was called"); },
+      construct() { throw new Error("proxy construct trap was called"); }
+    };
+    window.proxy = new Proxy(target, handler);
+
+    window.doPause = function () {
+      var proxy = window.proxy;
+      debugger;
+    };
+    </script>
+  </body>
+
+</html>
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -325,16 +325,30 @@ VariablesViewController.prototype = {
    * when a scope is expanded or certain variables are hovered.
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The grip to use to populate the target.
    */
   _populateFromObject: function (aTarget, aGrip) {
+    if (aGrip.class === "Proxy") {
+      this.addExpander(
+        aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
+        aGrip.proxyTarget);
+      this.addExpander(
+        aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
+        aGrip.proxyHandler);
+
+      // Refuse to play the proxy's stupid game and return immediately
+      let deferred = defer();
+      deferred.resolve();
+      return deferred.promise;
+    }
+    
     if (aGrip.class === "Promise" && aGrip.promiseState) {
       const { state, value, reason } = aGrip.promiseState;
       aTarget.addItem("<state>", { value: state }, { internalItem: true });
       if (state === "fulfilled") {
         this.addExpander(
           aTarget.addItem("<value>", { value }, { internalItem: true }),
           value);
       } else if (state === "rejected") {
--- a/devtools/client/webconsole/test/browser_webconsole_output_05.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_05.js
@@ -145,16 +145,25 @@ var inputTests = [
   {
     input: "new Object({1: 'this\\nis\\nsupposed\\nto\\nbe\\na\\nvery" +
            "\\nlong\\nstring\\n,shown\\non\\na\\nsingle\\nline', " +
            "2: 'a shorter string', 3: 100})",
     output: 'Object { 1: "this is supposed to be a very long ' + ELLIPSIS +
             '", 2: "a shorter string", 3: 100 }',
     printOutput: "[object Object]",
     inspectable: false,
+  },
+
+  // 15
+  {
+    input: "new Proxy({a:1},[1,2,3])",
+    output: 'Proxy { <target>: Object, <handler>: Array[3] }',
+    printOutput: "[object Object]",
+    inspectable: true,
+    variablesViewLabel: "Proxy"
   }
 ];
 
 function test() {
   requestLongerTimeout(2);
   Task.spawn(function* () {
     let {tab} = yield loadTab(TEST_URI);
     let hud = yield openConsole(tab);
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -76,51 +76,60 @@ ObjectActor.prototype = {
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
   grip: function () {
     this.hooks.incrementGripDepth();
 
     let g = {
       "type": "object",
-      "class": this.obj.class,
-      "actor": this.actorID,
-      "extensible": this.obj.isExtensible(),
-      "frozen": this.obj.isFrozen(),
-      "sealed": this.obj.isSealed()
+      "actor": this.actorID
     };
 
-    if (this.obj.class != "DeadObject") {
-      if (this.obj.class == "Promise") {
+    // If it's a proxy, lie and tell that it belongs to an invented
+    // "Proxy" class, and avoid calling the [[IsExtensible]] trap
+    if(this.obj.isProxy) {
+      g.class = "Proxy";
+      g.proxyTarget = this.hooks.createValueGrip(this.obj.proxyTarget);
+      g.proxyHandler = this.hooks.createValueGrip(this.obj.proxyHandler);
+    } else {
+      g.class = this.obj.class;
+      g.extensible = this.obj.isExtensible();
+      g.frozen = this.obj.isFrozen();
+      g.sealed = this.obj.isSealed();
+    }
+
+    if (g.class != "DeadObject") {
+      if (g.class == "Promise") {
         g.promiseState = this._createPromiseState();
       }
 
       // FF40+: Allow to know how many properties an object has
       // to lazily display them when there is a bunch.
       // Throws on some MouseEvent object in tests.
       try {
         // Bug 1163520: Assert on internal functions
-        if (this.obj.class != "Function") {
+        if (!["Function", "Proxy"].includes(g.class)) {
           g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
         }
       } catch (e) {}
 
       let raw = this.obj.unsafeDereference();
 
       // If Cu is not defined, we are running on a worker thread, where xrays
       // don't exist.
       if (Cu) {
         raw = Cu.unwaiveXrays(raw);
       }
 
       if (!DevToolsUtils.isSafeJSObject(raw)) {
         raw = null;
       }
 
-      let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] ||
+      let previewers = DebuggerServer.ObjectActorPreviewers[g.class] ||
                        DebuggerServer.ObjectActorPreviewers.Object;
       for (let fn of previewers) {
         try {
           if (fn(this, g, raw)) {
             break;
           }
         } catch (e) {
           let msg = "ObjectActor.prototype.grip previewer function";
@@ -1331,16 +1340,33 @@ DebuggerServer.ObjectActorPreviewers = {
       entries.push([key, hooks.createValueGrip(value)]);
       if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
+
+  Proxy: [function ({obj, hooks}, grip, rawObj) {
+    grip.preview = {
+      kind: "Object",
+      ownProperties: Object.create(null),
+      ownPropertiesLength: 2
+    };
+
+    if (hooks.getGripDepth() > 1) {
+      return true;
+    }
+
+    grip.preview.ownProperties['<target>'] = {value: grip.proxyTarget};
+    grip.preview.ownProperties['<handler>'] = {value: grip.proxyHandler};
+
+    return true;
+  }],
 };
 
 /**
  * Generic previewer for classes wrapping primitives, like String,
  * Number and Boolean.
  *
  * @param string className
  *        Class name to expect.