Bug 1457711 - Catch errors thrown by console's property previewer; r=nchevobbe draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Sat, 28 Apr 2018 22:29:43 +0200
changeset 790790 c6290683d3959a06df517c84c9f30a4c10255b60
parent 789020 d2d518b1f8730eb61554df7179ef9a2aeed4d843
push id108590
push userbmo:oriol-bugzilla@hotmail.com
push dateWed, 02 May 2018 20:09:31 +0000
reviewersnchevobbe
bugs1457711
milestone61.0a1
Bug 1457711 - Catch errors thrown by console's property previewer; r=nchevobbe MozReview-Commit-ID: LKsYn5gSn58
devtools/shared/webconsole/js-property-provider.js
devtools/shared/webconsole/test/test_jsterm_autocomplete.html
--- a/devtools/shared/webconsole/js-property-provider.js
+++ b/devtools/shared/webconsole/js-property-provider.js
@@ -388,16 +388,19 @@ function getMatchedProps(obj, match) {
 function getMatchedPropsImpl(obj, match, {chainIterator, getProperties}) {
   let matches = new Set();
   let numProps = 0;
 
   // We need to go up the prototype chain.
   let iter = chainIterator(obj);
   for (obj of iter) {
     let props = getProperties(obj);
+    if (!props) {
+      continue;
+    }
     numProps += props.length;
 
     // If there are too many properties to event attempt autocompletion,
     // or if we have already added the max number, then stop looping
     // and return the partial set that has already been discovered.
     if (numProps >= MAX_AUTOCOMPLETE_ATTEMPTS ||
         matches.size >= MAX_AUTOCOMPLETIONS) {
       break;
@@ -454,40 +457,60 @@ function getExactMatchImpl(obj, name, {c
   }
   return undefined;
 }
 
 var JSObjectSupport = {
   chainIterator: function* (obj) {
     while (obj) {
       yield obj;
-      obj = Object.getPrototypeOf(obj);
+      try {
+        obj = Object.getPrototypeOf(obj);
+      } catch (error) {
+        // The above can throw e.g. for some proxy objects.
+        return;
+      }
     }
   },
 
   getProperties: function(obj) {
-    return Object.getOwnPropertyNames(obj);
+    try {
+      return Object.getOwnPropertyNames(obj);
+    } catch (error) {
+      // The above can throw e.g. for some proxy objects.
+      return null;
+    }
   },
 
   getProperty: function() {
     // getProperty is unsafe with raw JS objects.
     throw new Error("Unimplemented!");
   },
 };
 
 var DebuggerObjectSupport = {
   chainIterator: function* (obj) {
     while (obj) {
       yield obj;
-      obj = obj.proto;
+      try {
+        obj = obj.proto;
+      } catch (error) {
+        // The above can throw e.g. for some proxy objects.
+        return;
+      }
     }
   },
 
   getProperties: function(obj) {
-    return obj.getOwnPropertyNames();
+    try {
+      return obj.getOwnPropertyNames();
+    } catch (error) {
+      // The above can throw e.g. for some proxy objects.
+      return null;
+    }
   },
 
   getProperty: function(obj, name, rootObj) {
     // This is left unimplemented in favor to DevToolsUtils.getProperty().
     throw new Error("Unimplemented!");
   },
 };
 
--- a/devtools/shared/webconsole/test/test_jsterm_autocomplete.html
+++ b/devtools/shared/webconsole/test/test_jsterm_autocomplete.html
@@ -18,34 +18,34 @@ let gState;
 let {MAX_AUTOCOMPLETE_ATTEMPTS,MAX_AUTOCOMPLETIONS} = require("devtools/shared/webconsole/js-property-provider");
 
 function evaluateJS(input, options = {}) {
   return new Promise((resolve, reject) => {
     gState.client.evaluateJSAsync(input, resolve, options);
   });
 }
 
-function autocompletePromise(str, cursor, frameActor) {
+function autocompletePromise(str, cursor = str.length, frameActor) {
   return new Promise(resolve => {
     gState.client.autocomplete(str, cursor, resolve, frameActor);
   });
 }
 
 // This test runs all of its assertions twice - once with
 // the tab as a target and once with a worker
 let runningInTab = true;
 function startTest({worker}) {
   if (worker) {
-    attachConsoleToWorker(["PageError"], onAttach);
+    attachConsoleToWorker(["PageError"], onAttach.bind(null, true));
   } else {
-    attachConsoleToTab(["PageError"], onAttach);
+    attachConsoleToTab(["PageError"], onAttach.bind(null, false));
   }
 };
 
-let onAttach = async function (aState, response) {
+let onAttach = async function (isWorker, aState, response) {
   gState = aState;
 
   let longStrLength = DebuggerServer.LONG_STRING_LENGTH;
 
   // Set up the global variables needed to test autocompletion
   // in the target.
   let script = `
     // This is for workers so autocomplete acts the same
@@ -66,42 +66,54 @@ let onAttach = async function (aState, r
     for (let i = 0; i < ${MAX_AUTOCOMPLETE_ATTEMPTS + 1}; i++) {
       window.largeObject1['a' + i] = i;
     }
 
     window.largeObject2 = Object.create(null);
     for (let i = 0; i < ${MAX_AUTOCOMPLETIONS * 2}; i++) {
       window.largeObject2['a' + i] = i;
     }
+
+    window.proxy1 = new Proxy({foo: 1}, {
+      getPrototypeOf() { throw new Error() }
+    });
+    window.proxy2 = new Proxy(Object.create(Object.create(null, {foo:{}})), {
+      ownKeys() { throw new Error() }
+    });
   `;
 
   await evaluateJS(script);
 
   let tests = [doAutocomplete1, doAutocomplete2, doAutocomplete3,
                doAutocomplete4, doAutocompleteLarge1,
-               doAutocompleteLarge2];
+               doAutocompleteLarge2, doAutocompleteProxyThrowsPrototype,
+               doAutocompleteProxyThrowsOwnKeys];
+  if (!isWorker) {
+    // `Cu` is not defined in workers, then we can't test `Cu.Sandbox`
+    tests.push(doAutocompleteSandbox);
+  }
 
   runTests(tests, testEnd);
 };
 
 async function doAutocomplete1() {
   info("test autocomplete for 'window.foo'");
-  let response = await autocompletePromise("window.foo", 10);
+  let response = await autocompletePromise("window.foo");
   let matches = response.matches;
 
   is(response.matchProp, "foo", "matchProp");
   is(matches.length, 1, "matches.length");
   is(matches[0], "foobarObject", "matches[0]");
 
   nextTest();
 }
 
 async function doAutocomplete2() {
   info("test autocomplete for 'window.foobarObject.'");
-  let response = await autocompletePromise("window.foobarObject.", 20);
+  let response = await autocompletePromise("window.foobarObject.");
   let matches = response.matches;
 
   ok(!response.matchProp, "matchProp");
   is(matches.length, 7, "matches.length");
   checkObject(matches,
     ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
 
   nextTest();
@@ -119,46 +131,80 @@ async function doAutocomplete3() {
     ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]);
 
   nextTest();
 }
 
 async function doAutocomplete4() {
   // Check that completion requests can have no suggestions.
   info("test autocomplete for 'dump(window.foobarObject.)'");
-  let response = await autocompletePromise("dump(window.foobarObject.)", 26);
+  let response = await autocompletePromise("dump(window.foobarObject.)");
   ok(!response.matchProp, "matchProp");
   is(response.matches.length, 0, "matches.length");
 
   nextTest();
 }
 
 async function doAutocompleteLarge1() {
   // Check that completion requests with too large objects will
   // have no suggestions.
   info("test autocomplete for 'window.largeObject1.'");
-  let response = await autocompletePromise("window.largeObject1.", 20);
+  let response = await autocompletePromise("window.largeObject1.");
   ok(!response.matchProp, "matchProp");
   info (response.matches.join("|"));
   is(response.matches.length, 0, "Bailed out with too many properties");
 
   nextTest();
 }
 
 async function doAutocompleteLarge2() {
   // Check that completion requests with pretty large objects will
   // have MAX_AUTOCOMPLETIONS suggestions
   info("test autocomplete for 'window.largeObject2.'");
-  let response = await autocompletePromise("window.largeObject2.", 20);
+  let response = await autocompletePromise("window.largeObject2.");
   ok(!response.matchProp, "matchProp");
   is(response.matches.length, MAX_AUTOCOMPLETIONS, "matches.length is MAX_AUTOCOMPLETIONS");
 
   nextTest();
 }
 
+async function doAutocompleteProxyThrowsPrototype() {
+  // Check that completion provides own properties even if [[GetPrototypeOf]] throws.
+  info("test autocomplete for 'window.proxy1.'");
+  let response = await autocompletePromise("window.proxy1.");
+  ok(!response.matchProp, "matchProp");
+  is(response.matches.length, 1, "matches.length");
+  checkObject(response.matches, ["foo"]);
+
+  nextTest();
+}
+
+async function doAutocompleteProxyThrowsOwnKeys() {
+  // Check that completion provides inherited properties even if [[OwnPropertyKeys]] throws.
+  info("test autocomplete for 'window.proxy2.'");
+  let response = await autocompletePromise("window.proxy2.");
+  ok(!response.matchProp, "matchProp");
+  is(response.matches.length, 1, "matches.length");
+  checkObject(response.matches, ["foo"]);
+
+  nextTest();
+}
+
+async function doAutocompleteSandbox() {
+  // Check that completion provides inherited properties even if [[OwnPropertyKeys]] throws.
+  info("test autocomplete for 'Cu.Sandbox.'");
+  let response = await autocompletePromise("Cu.Sandbox.");
+  ok(!response.matchProp, "matchProp");
+  let keys = Object.getOwnPropertyNames(Object.prototype).sort();
+  is(response.matches.length, keys.length, "matches.length");
+  checkObject(response.matches, keys);
+
+  nextTest();
+}
+
 function testEnd()
 {
   // If this is the first run, reload the page and do it again
   // in a worker.  Otherwise, end the test.
   closeDebugger(gState, function() {
     gState = null;
     if (runningInTab) {
       runningInTab = false;