Web Inspector: Console could be made useful for very simple await expressions
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 20 Dec 2016 21:41:03 +0000 (21:41 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 20 Dec 2016 21:41:03 +0000 (21:41 +0000)
https://bugs.webkit.org/show_bug.cgi?id=165681
<rdar://problem/29755339>

Reviewed by Brian Burg.

Source/WebInspectorUI:

Normally await expressions are only allowed inside of async functions.
They make dealing with async operations easy, but can't be used directly
in Web Inspector's console without making your own async function wrapper.

This change allows simple await expressions to be run in the console.
The supported syntaxes are (simple expression with optional assignment):

    await <expr>
    x = await <expr>
    let x = await <expr>

Web Inspector's console will automatically wrap this in an async
function and report the resulting value or exception. For instance
in the last example above:

    let x;
    (async function() {
        try {
            x = await <expr>;
            console.info("%o", x);
        } catch (e) {
            console.error(e);
        }
    })();
    undefined

This way users can get the convenience of await in the Console.
This also gives users a nice way of extracting a value out of
a Promise without writing their own handlers.

* UserInterface/Controllers/RuntimeManager.js:
(WebInspector.RuntimeManager.prototype.evaluateInInspectedWindow):
(WebInspector.RuntimeManager.prototype._tryApplyAwaitConvenience):
Wrap simple await expressions into a function that will log the result.

LayoutTests:

* inspector/controller/runtime-controller-expected.txt:
* inspector/controller/runtime-controller.html:
Test the "await expression" convenience of RuntimeManager.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@210033 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/inspector/controller/runtime-controller-expected.txt
LayoutTests/inspector/controller/runtime-controller.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Controllers/BreakpointLogMessageLexer.js
Source/WebInspectorUI/UserInterface/Controllers/RuntimeManager.js

index 45b05ad..b5aca7f 100644 (file)
@@ -1,3 +1,15 @@
+2016-12-20  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Console could be made useful for very simple await expressions
+        https://bugs.webkit.org/show_bug.cgi?id=165681
+        <rdar://problem/29755339>
+
+        Reviewed by Brian Burg.
+
+        * inspector/controller/runtime-controller-expected.txt:
+        * inspector/controller/runtime-controller.html:
+        Test the "await expression" convenience of RuntimeManager.
+
 2016-12-20  Ryan Haddad  <ryanhaddad@apple.com>
 
         Rebaseline js/dom/global-constructors-attributes.html for mac-elcapitan after r210024.
index 55c1b79..ded82fe 100644 (file)
@@ -1,4 +1,13 @@
-Tests for the Runtime.parse command.
+CONSOLE MESSAGE: line 7: %o
+CONSOLE MESSAGE: line 7: %o
+CONSOLE MESSAGE: line 7: %o
+CONSOLE MESSAGE: line 9: Thrown exception
+CONSOLE MESSAGE: line 7: %o
+CONSOLE MESSAGE: line 9: Promise.reject
+CONSOLE MESSAGE: line 9: Rejection
+CONSOLE MESSAGE: line 7: %o
+CONSOLE MESSAGE: line 7: %o
+Tests for RuntimeManager operations.
 
 
 == Running test suite: RuntimeManager
@@ -20,3 +29,32 @@ PASS: Evaluation should produce an exception.
 Source: ;{ let a = 1; a += 1; a }
 PASS: Evaluation should produce the labeled statement's value.
 
+-- Running test case: RuntimeManager.prototype.evaluateInInspectedWindow.AwaitConvenience
+
+Source: await 1
+PASS: Transformed. Should log the value or an exception.
+Source:    await 2   
+PASS: Transformed. Should log the value or an exception.
+Source: var x = await 3
+PASS: Transformed. Should log the value or an exception.
+Source: await causeExceptionImmediately()
+PASS: Transformed. Should log the value or an exception.
+Source: await Promise.resolve(4)
+PASS: Transformed. Should log the value or an exception.
+Source: await Promise.reject('Promise.reject')
+PASS: Transformed. Should log the value or an exception.
+Source: await rejectedEventually()
+PASS: Transformed. Should log the value or an exception.
+Source: await asyncOperation()
+PASS: Transformed. Should log the value or an exception.
+Source: x = await asyncOperation()
+PASS: Transformed. Should log the value or an exception.
+Source: return 10
+PASS: Exception. Should not get transformed and produce a SyntaxError.
+Source: await 10; 1
+PASS: Exception. Should not get transformed and produce a SyntaxError.
+Source: 1; await 10;
+PASS: Exception. Should not get transformed and produce a SyntaxError.
+Source: x = y = await 10
+PASS: Exception. Should not get transformed and produce a SyntaxError.
+
index bbebba0..4808bd6 100644 (file)
@@ -3,6 +3,27 @@
 <head>
 <script src="../../http/tests/inspector/resources/inspector-test.js"></script>
 <script>
+let resultNumber = 100;
+function asyncOperation() {
+    return new Promise((resolve, reject) => {
+        setTimeout(() => {
+            resolve(resultNumber++);
+        }, 50);
+    });
+}
+
+function rejectedEventually() {
+    return new Promise((resolve, reject) => {
+        setTimeout(() => {
+            reject("Rejection");
+        }, 50);
+    });
+}
+
+function causeExceptionImmediately() {
+    throw "Thrown exception";
+}
+
 function test()
 {
     let suite = InspectorTest.createAsyncSuite("RuntimeManager");
@@ -10,7 +31,7 @@ function test()
     suite.addTestCase({
         name: "RuntimeManager.prototype.evaluateInInspectedWindow.ObjectLiteralConvenience",
         description: "Test evaluating an object literal string conveniently converts wraps it in parenthesis to avoid misinterpretation as a program with a block and labeled statement.",
-        test: (resolve, reject) => {
+        test(resolve, reject) {
             function testSource(expression, callback) {
                 WebInspector.runtimeManager.evaluateInInspectedWindow(expression, {objectGroup: "test"}, (result, wasThrown) => {
                     InspectorTest.log("Source: " + expression);
@@ -50,11 +71,64 @@ function test()
         }
     });
 
+    suite.addTestCase({
+        name: "RuntimeManager.prototype.evaluateInInspectedWindow.AwaitConvenience",
+        description: "Test evaluating a simple await expression wraps it into an async function and eventually resolves the value.",
+        test(resolve, reject) {
+            function testSource(expression, callback) {
+                WebInspector.runtimeManager.evaluateInInspectedWindow(expression, {objectGroup: "test"}, (result, wasThrown) => {
+                    InspectorTest.log("Source: " + expression);
+                    callback(result, wasThrown);
+                });
+            }
+
+            function expectUndefined(result, wasThrown) {
+                InspectorTest.expectThat(result.isUndefined(), "Transformed. Should log the value or an exception.");
+            }
+
+            function expectException(result, wasThrown) {
+                InspectorTest.expectThat(wasThrown, "Exception. Should not get transformed and produce a SyntaxError.");
+            }
+
+            // The convenience will detect these and make an async task.
+            const expected = 6;
+            testSource("await 1", expectUndefined);
+            testSource("   await 2   ", expectUndefined);
+            testSource("var x = await 3", expectUndefined);
+            testSource("await causeExceptionImmediately()", expectUndefined);
+            testSource("await Promise.resolve(4)", expectUndefined);
+            testSource("await Promise.reject('Promise.reject')", expectUndefined);
+            testSource("await rejectedEventually()", expectUndefined);
+            testSource("await asyncOperation()", expectUndefined);
+            testSource("x = await asyncOperation()", expectUndefined);
+
+            InspectorTest.log("");
+
+            // The convenience will not apply to these noexpressions.
+            testSource("return 10", expectException);
+            testSource("await 10; 1", expectException);
+            testSource("1; await 10;", expectException);
+            testSource("x = y = await 10", expectException);
+
+            // We can resolve after receiving the expected number of console.info messages.
+            let seen = 0;
+            let listener = WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.MessageAdded, (event) => {
+                let consoleMessage = event.data.message;
+                if (consoleMessage.level !== WebInspector.ConsoleMessage.MessageLevel.Info)
+                    return;
+                if (++seen !== expected)
+                    return;
+                WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.MessageAdded, listener);
+                resolve();
+            });
+        }
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>
 </head>
 <body onload="runTest()">
-<p>Tests for the Runtime.parse command.</p>
+<p>Tests for RuntimeManager operations.</p>
 </body>
 </html>
index 17746a7..0398648 100644 (file)
@@ -1,5 +1,48 @@
 2016-12-20  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: Console could be made useful for very simple await expressions
+        https://bugs.webkit.org/show_bug.cgi?id=165681
+        <rdar://problem/29755339>
+
+        Reviewed by Brian Burg.
+
+        Normally await expressions are only allowed inside of async functions.
+        They make dealing with async operations easy, but can't be used directly
+        in Web Inspector's console without making your own async function wrapper.
+
+        This change allows simple await expressions to be run in the console.
+        The supported syntaxes are (simple expression with optional assignment):
+
+            await <expr>
+            x = await <expr>
+            let x = await <expr>
+
+        Web Inspector's console will automatically wrap this in an async
+        function and report the resulting value or exception. For instance
+        in the last example above:
+
+            let x;
+            (async function() {
+                try {
+                    x = await <expr>;
+                    console.info("%o", x);
+                } catch (e) {
+                    console.error(e);
+                }
+            })();
+            undefined
+
+        This way users can get the convenience of await in the Console.
+        This also gives users a nice way of extracting a value out of
+        a Promise without writing their own handlers.
+
+        * UserInterface/Controllers/RuntimeManager.js:
+        (WebInspector.RuntimeManager.prototype.evaluateInInspectedWindow):
+        (WebInspector.RuntimeManager.prototype._tryApplyAwaitConvenience):
+        Wrap simple await expressions into a function that will log the result.
+
+2016-12-20  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Update CodeMirror to support async/await keyword and other ES2017 features
         https://bugs.webkit.org/show_bug.cgi?id=165677
 
index 9c24cb2..071704a 100644 (file)
@@ -152,7 +152,7 @@ WebInspector.BreakpointLogMessageLexer = class BreakpointLogMessageLexer extends
     _possiblePlaceholder()
     {
         let character = this._consume();
-        console.assert(character === "$")
+        console.assert(character === "$");
         let nextCharacter = this._peek();
 
         console.assert(this._states.lastValue === WebInspector.BreakpointLogMessageLexer.State.PossiblePlaceholder);
index b8c4fe4..a105414 100644 (file)
@@ -74,6 +74,9 @@ WebInspector.RuntimeManager = class RuntimeManager extends WebInspector.Object
         } else if (/^\s*\{/.test(expression) && /\}\s*$/.test(expression)) {
             // Transform {a:1} to ({a:1}) so it is treated like an object literal instead of a block with a label.
             expression = "(" + expression + ")";
+        } else if (/\bawait\b/.test(expression)) {
+            // Transform `await <expr>` into an async function assignment.
+            expression = this._tryApplyAwaitConvenience(expression);
         }
 
         expression = sourceURLAppender(expression);
@@ -162,6 +165,91 @@ WebInspector.RuntimeManager = class RuntimeManager extends WebInspector.Object
         if (currentContextWasDestroyed)
             this.activeExecutionContext = WebInspector.mainTarget.executionContext;
     }
+
+    _tryApplyAwaitConvenience(originalExpression)
+    {
+        let esprimaSyntaxTree;
+
+        // Do not transform if the original code parses just fine.
+        try {
+            esprima.parse(originalExpression);
+            return originalExpression;
+        } catch (error) { }
+
+        // Do not transform if the async function version does not parse.
+        try {
+            esprimaSyntaxTree = esprima.parse("(async function(){" + originalExpression + "})");
+        } catch (error) {
+            return originalExpression;
+        }
+
+        // Assert expected AST produced by our wrapping code.
+        console.assert(esprimaSyntaxTree.type === "Program");
+        console.assert(esprimaSyntaxTree.body.length === 1);
+        console.assert(esprimaSyntaxTree.body[0].type === "ExpressionStatement");
+        console.assert(esprimaSyntaxTree.body[0].expression.type === "FunctionExpression");
+        console.assert(esprimaSyntaxTree.body[0].expression.async);
+        console.assert(esprimaSyntaxTree.body[0].expression.body.type === "BlockStatement");
+
+        // Do not transform if there is more than one statement.
+        let asyncFunctionBlock = esprimaSyntaxTree.body[0].expression.body;
+        if (asyncFunctionBlock.body.length !== 1)
+            return originalExpression;
+
+        // Extract the variable name for transformation.
+        let variableName;
+        let anonymous = false;
+        let declarationKind = "var";
+        let awaitPortion;
+        let statement = asyncFunctionBlock.body[0];
+        if (statement.type === "ExpressionStatement"
+            && statement.expression.type === "AwaitExpression") {
+            // await <expr>
+            anonymous = true;
+        } else if (statement.type === "ExpressionStatement"
+            && statement.expression.type === "AssignmentExpression"
+            && statement.expression.right.type === "AwaitExpression"
+            && statement.expression.left.type === "Identifier") {
+            // x = await <expr>
+            variableName = statement.expression.left.name;
+            awaitPortion = originalExpression.substring(originalExpression.indexOf("await"));
+        } else if (statement.type === "VariableDeclaration"
+            && statement.declarations.length === 1
+            && statement.declarations[0].init.type === "AwaitExpression"
+            && statement.declarations[0].id.type === "Identifier") {
+            // var x = await <expr>
+            variableName = statement.declarations[0].id.name;
+            declarationKind = statement.kind;
+            awaitPortion = originalExpression.substring(originalExpression.indexOf("await"));
+        } else {
+            // Do not transform if this was not one of the simple supported syntaxes.
+            return originalExpression;
+        }
+
+        if (anonymous) {
+            return `
+(async function() {
+    try {
+        let result = ${originalExpression};
+        console.info("%o", result);
+    } catch (e) {
+        console.error(e);
+    }
+})();
+undefined`;
+        }
+
+        return `${declarationKind} ${variableName};
+(async function() {
+    try {
+        ${variableName} = ${awaitPortion};
+        console.info("%o", ${variableName});
+    } catch (e) {
+        console.error(e);
+    }
+})();
+undefined;`;
+    }
 };
 
 WebInspector.RuntimeManager.ConsoleObjectGroup = "console";