Bug 1246215 - Console prevents let re-declaration even when first was an error; r=jryans,shu
authorMorgan Phillips <winter2718@gmail.com>
Wed, 24 Feb 2016 12:16:16 -0600
changeset 321781 6104903f75c5f154cc6c5e8d89dbf0a279284e59
parent 321780 2bab88e97af5f29f32861135af8481412ee50499
child 321782 615be94be0865921a73a7d2f81fbdb6bb1640bc6
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans, shu
bugs1246215
milestone47.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 1246215 - Console prevents let re-declaration even when first was an error; r=jryans,shu
devtools/server/actors/webconsole.js
devtools/shared/webconsole/client.js
devtools/shared/webconsole/test/test_jsterm.html
js/src/doc/Debugger/Debugger.Object.md
js/src/jit-test/tests/debug/Object-forceLexicalInitializationByName.js
js/src/vm/Debugger.cpp
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -14,16 +14,17 @@ const { ObjectActor, LongStringActor, cr
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 
 loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "ServerLoggingListener", "devtools/shared/webconsole/server-logger", true);
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
+loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
     "ConsoleAPIListener", "addWebConsoleCommands",
     "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
   Object.defineProperty(this, name, {
     get: function(prop) {
       if (prop == "WebConsoleUtils") {
         prop = "Utils";
@@ -1272,16 +1273,37 @@ WebConsoleActor.prototype =
     }
 
     let result;
     if (frame) {
       result = frame.evalWithBindings(aString, bindings, evalOptions);
     }
     else {
       result = dbgWindow.executeInGlobalWithBindings(aString, bindings, evalOptions);
+      // Attempt to initialize any declarations found in the evaluated string
+      // since they may now be stuck in an "initializing" state due to the
+      // error. Already-initialized bindings will be ignored.
+      if ("throw" in result) {
+        let ast;
+        // Parse errors will raise an exception. We can/should ignore the error
+        // since it's already being handled elsewhere and we are only interested
+        // in initializing bindings.
+        try {
+          ast = Parser.reflectionAPI.parse(aString);
+        } catch (ex) {
+          ast = {"body": []};
+        }
+        for (let line of ast.body) {
+          if (line.type == "VariableDeclaration" &&
+            (line.kind == "let" || line.kind == "const")) {
+            for (let decl of line.declarations)
+              dbgWindow.forceLexicalInitializationByName(decl.id.name);
+          }
+        }
+      }
     }
 
     let helperResult = helpers.helperResult;
     delete helpers.evalInput;
     delete helpers.helperResult;
     delete helpers.selectedNode;
 
     if ($) {
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -1,10 +1,10 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft= javascript ts=2 et sw=2 tw=80: */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
--- a/devtools/shared/webconsole/test/test_jsterm.html
+++ b/devtools/shared/webconsole/test/test_jsterm.html
@@ -61,17 +61,18 @@ function onAttach(aState, aResponse)
   for (let i = 0; i < MAX_AUTOCOMPLETIONS * 2; i++) {
     top.largeObject2['a' + i] = i;
   }
 
   gState = aState;
 
   let tests = [doSimpleEval, doWindowEval, doEvalWithException,
                doEvalWithHelper, doEvalString, doEvalLongString,
-               doEvalWithBinding, doEvalWithBindingFrame].map(t => {
+               doEvalWithBinding, doEvalWithBindingFrame,
+               forceLexicalInit].map(t => {
                  return Task.async(t);
                });
 
   runTests(tests, testEnd);
 }
 
 function* doSimpleEval() {
   info("test eval '2+2'");
@@ -218,16 +219,35 @@ function* doEvalWithBindingFrame() {
   ok(top.temp1 && top.temp1.bar === 1,
     "Global matches the top global with bindObjectActor");
   ok(!frameWin.temp1,
     "Global doesn't match the object's global with bindObjectActor");
 
   nextTest()
 }
 
+function* forceLexicalInit() {
+  info("test `let x = SomeError` results in x being initialized to undefined");
+  let response = yield evaluateJS("let foopie = wubbalubadubdub;");
+  checkObject(response, {
+    from: gState.actor,
+    input: "let foopie = wubbalubadubdub;",
+    result: undefined,
+  });
+  ok(response.exception, "expected exception");
+  let response2 = yield evaluateJS("foopie;");
+  checkObject(response2, {
+    from: gState.actor,
+    input: "foopie;",
+    result: undefined,
+  });
+  ok(!response2.exception, "unexpected exception");
+  nextTest();
+}
+
 function testEnd()
 {
   // If this is the first run, reload the page and do it again.
   // Otherwise, end the test.
   closeDebugger(gState, function() {
     gState = null;
     if (evaluatingSync) {
       evaluatingSync = false;
--- a/js/src/doc/Debugger/Debugger.Object.md
+++ b/js/src/doc/Debugger/Debugger.Object.md
@@ -539,8 +539,12 @@ code), the call throws a [`Debugger.Debu
     This method pierces the membrane of `Debugger.Object` instances meant to
     protect debugger code from debuggee code, and allows debugger code to
     access debuggee objects through the standard cross-compartment wrappers,
     rather than via `Debugger.Object`'s reflection-oriented interfaces. This
     method makes it easier to gradually adapt large code bases to this
     Debugger API: adapted portions of the code can use `Debugger.Object`
     instances, but use this method to pass direct object references to code
     that has not yet been updated.
+
+<code>forceLexicalInitializationByName(<i>binding</i>)</code>
+:  If <i>binding</i> is in an uninitialized state initialize it to undefined
+   and return true, otherwise do nothing and return false.
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Object-forceLexicalInitializationByName.js
@@ -0,0 +1,53 @@
+load(libdir + "asserts.js");
+
+var g = newGlobal();
+var dbg = new Debugger;
+var gw = dbg.addDebuggee(g);
+
+let errorOne, errorTwo;
+
+function evalErrorStr(global, evalString) {
+    try {
+        global.evaluate(evalString);
+        return undefined;
+    } catch (e) {
+        return e.toString();
+    }
+}
+
+
+assertEq(evalErrorStr(g, "let y = IDONTEXIST;"), "ReferenceError: IDONTEXIST is not defined");
+
+assertEq(evalErrorStr(g, "y = 1;"),
+         "ReferenceError: can't access lexical declaration `y' before initialization");
+
+// Here we flip the uninitialized binding to undfined.
+assertEq(gw.forceLexicalInitializationByName("y"), true);
+assertEq(g.evaluate("y"), undefined);
+g.evaluate("y = 1;");
+assertEq(g.evaluate("y"), 1);
+
+// Ensure that bogus bindings return false, but otherwise trigger no error or
+// side effect.
+assertEq(gw.forceLexicalInitializationByName("idontexist"), false);
+assertEq(evalErrorStr(g, "idontexist"), "ReferenceError: idontexist is not defined");
+
+// Ensure that only strings are accepted by forceLexicalInitializationByName
+const bad_types = [
+    2112,
+    {geddy: "lee"},
+    () => 1,
+    [],
+    Array
+]
+
+for (var badType of bad_types) {
+    assertThrowsInstanceOf(() => {
+        gw.forceLexicalInitializationByName(badType);
+    }, TypeError);
+}
+
+// Finally, supplying no arguments should throw a type error
+assertThrowsInstanceOf(() => {
+    Debugger.isCompilableUnit();
+}, TypeError);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -4762,16 +4762,17 @@ Debugger::isCompilableUnit(JSContext* cx
 
         cx->clearPendingException();
     }
     JS_SetErrorReporter(cx->runtime(), older);
     args.rval().setBoolean(result);
     return true;
 }
 
+
 bool
 Debugger::drainTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGGER(cx, argc, vp, "drainTraceLoggerScriptCalls", args, dbg);
     if (!args.requireAtLeast(cx, "Debugger.drainTraceLoggerScriptCalls", 0))
         return false;
 
     size_t num;
@@ -8027,16 +8028,58 @@ RequireGlobalObject(JSContext* cx, Handl
                                   "a global object", nullptr);
         }
         return false;
     }
 
     return true;
 }
 
+// Lookup a binding on the referent's global scope and change it to undefined
+// if it is an uninitialized lexical, otherwise do nothing. The method's return
+// value is true _only_ when an uninitialized lexical has been altered, otherwise
+// it is false.
+bool
+DebuggerObject_forceLexicalInitializationByName(JSContext *cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "forceLexicalInitializationByname", args, referent);
+    if (!args.requireAtLeast(cx, "Debugger.Object.prototype.forceLexicalInitializationByName", 1))
+        return false;
+    if (!RequireGlobalObject(cx, args.thisv(), referent))
+        return false;
+
+    RootedObject globalLexical(cx, &referent->as<GlobalObject>().lexicalScope());
+
+    if (!args[0].isString()) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
+                             JSMSG_NOT_EXPECTED_TYPE, "Debugger.Object.prototype.forceLexicalInitializationByName",
+                             "string", InformalValueTypeName(args[0]));
+        return false;
+    }
+
+    PropertyName* name = args[0].toString()->asAtom().asPropertyName();
+
+    bool initialized = false;
+
+    Shape* s = nullptr;
+    JSObject* scope = nullptr;
+    JSObject* pobj = nullptr;
+
+    if (LookupNameNoGC(cx, name, globalLexical, &scope, &pobj, &s)) {
+        Value v = globalLexical->as<NativeObject>().getSlot(s->slot());
+        if (s->hasSlot() && v.isMagic() && v.whyMagic() == JS_UNINITIALIZED_LEXICAL) {
+            globalLexical->as<NativeObject>().setSlot(s->slot(), UndefinedValue());
+            initialized = true;
+         }
+    }
+
+    args.rval().setBoolean(initialized);
+    return true;
+}
+
 static bool
 DebuggerObject_executeInGlobal(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "executeInGlobal", args, dbg, referent);
     if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobal", 1))
         return false;
     if (!RequireGlobalObject(cx, args.thisv(), referent))
         return false;
@@ -8146,16 +8189,17 @@ static const JSFunctionSpec DebuggerObje
     JS_FN("getOwnPropertySymbols", DebuggerObject_getOwnPropertySymbols, 0, 0),
     JS_FN("defineProperty", DebuggerObject_defineProperty, 2, 0),
     JS_FN("defineProperties", DebuggerObject_defineProperties, 1, 0),
     JS_FN("deleteProperty", DebuggerObject_deleteProperty, 1, 0),
     JS_FN("seal", DebuggerObject_seal, 0, 0),
     JS_FN("freeze", DebuggerObject_freeze, 0, 0),
     JS_FN("preventExtensions", DebuggerObject_preventExtensions, 0, 0),
     JS_FN("isSealed", DebuggerObject_isSealed, 0, 0),
+    JS_FN("forceLexicalInitializationByName", DebuggerObject_forceLexicalInitializationByName, 1, 0),
     JS_FN("isFrozen", DebuggerObject_isFrozen, 0, 0),
     JS_FN("isExtensible", DebuggerObject_isExtensible, 0, 0),
     JS_FN("apply", DebuggerObject_apply, 0, 0),
     JS_FN("call", DebuggerObject_call, 0, 0),
     JS_FN("makeDebuggeeValue", DebuggerObject_makeDebuggeeValue, 1, 0),
     JS_FN("executeInGlobal", DebuggerObject_executeInGlobal, 1, 0),
     JS_FN("executeInGlobalWithBindings", DebuggerObject_executeInGlobalWithBindings, 2, 0),
     JS_FN("asEnvironment", DebuggerObject_asEnvironment, 0, 0),