Skip to content

Commit

Permalink
Fix rewrite of nested async arrow functions that have references to "…
Browse files Browse the repository at this point in the history
…this", "super" or "arguments".

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=185432876
  • Loading branch information
SergejSalnikov authored and brad4d committed Feb 13, 2018
1 parent cbbee4a commit 8f8b4b0
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 51 deletions.
91 changes: 47 additions & 44 deletions src/com/google/javascript/jscomp/RewriteAsyncFunctions.java
Expand Up @@ -18,7 +18,6 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Optional;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
Expand All @@ -27,6 +26,7 @@
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.Nullable;

/**
* Converts async functions to valid ES6 generator functions code.
Expand Down Expand Up @@ -65,53 +65,59 @@ public final class RewriteAsyncFunctions implements NodeTraversal.Callback, HotS
* for the function currently in context.
*/
private static final class LexicalContext {
final Optional<Node> function; // absent for top level
final LexicalContext thisAndArgumentsContext;
@Nullable final Node function;
@Nullable final LexicalContext asyncThisAndArgumentsContext;
final Set<String> replacedSuperProperties = new LinkedHashSet<>();
boolean mustAddAsyncThisVariable = false;
boolean mustAddAsyncArgumentsVariable = false;

/** Creates root-level context. */
LexicalContext() {
this.function = Optional.absent();
this.thisAndArgumentsContext = this;
function = null;
asyncThisAndArgumentsContext = null;
}

LexicalContext(LexicalContext outer, Node function) {
this.function = Optional.of(function);
// An arrow function shares 'this' and 'arguments' with its outer scope.
this.thisAndArgumentsContext =
function.isArrowFunction() ? outer.thisAndArgumentsContext : this;
this.function = function;

if (function.isAsyncFunction()) {
if (function.isArrowFunction()) {
// An async arrow function context points to outer.asyncThisAndArgumentsContext
// if non-null, otherwise to itself.
asyncThisAndArgumentsContext = outer.asyncThisAndArgumentsContext == null
? this : outer.asyncThisAndArgumentsContext;
} else {
// An async non-arrow function context always points to itself
asyncThisAndArgumentsContext = this;
}
} else {
if (function.isArrowFunction()) {
// A non-async arrow function context always points to outer.asyncThisAndArgumentsContext
asyncThisAndArgumentsContext = outer.asyncThisAndArgumentsContext;
} else {
// A non-async, non-arrow function has no async context.
asyncThisAndArgumentsContext = null;
}
}
}

boolean isAsyncContext() {
return function.isPresent() && function.get().isAsyncFunction();
return function.isAsyncFunction();
}

boolean mustReplaceThisAndArguments() {
return isAsyncContext() || thisAndArgumentsContext.isAsyncContext();
}

LexicalContext getAsyncThisAndArgumentsContext() {
if (thisAndArgumentsContext.isAsyncContext()) {
return thisAndArgumentsContext;
}
// The current context is an async arrow function within a non-async function,
// so it must define its own replacement variables.
checkState(isAsyncContext());
return this;
return asyncThisAndArgumentsContext != null;
}

void recordAsyncThisReplacementWasDone() {
getAsyncThisAndArgumentsContext().mustAddAsyncThisVariable = true;
asyncThisAndArgumentsContext.mustAddAsyncThisVariable = true;
}

void recordAsyncSuperReplacementWasDone(String superFunctionName) {
getAsyncThisAndArgumentsContext().replacedSuperProperties.add(superFunctionName);
asyncThisAndArgumentsContext.replacedSuperProperties.add(superFunctionName);
}

void recordAsyncArgumentsReplacementWasDone() {
getAsyncThisAndArgumentsContext().mustAddAsyncArgumentsVariable = true;
asyncThisAndArgumentsContext.mustAddAsyncArgumentsVariable = true;
}
}

Expand Down Expand Up @@ -143,7 +149,7 @@ public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent)
if (n.isFunction()) {
contextStack.addFirst(new LexicalContext(contextStack.getFirst(), n));
if (n.isAsyncFunction()) {
compiler.ensureLibraryInjected("es6/execute_async_generator", /* force */ false);
compiler.ensureLibraryInjected("es6/execute_async_generator", /* force= */ false);
}
}
return true;
Expand All @@ -152,17 +158,14 @@ public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent)
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
LexicalContext context = contextStack.getFirst();

if (n.isFunction()) {
checkState(
context.function.isPresent() && context.function.get() == n,
"unexpected function context:\nexpected: %s\nactual: %s",
n,
context.function);
contextStack.removeFirst();
}
switch (n.getToken()) {
case FUNCTION:
checkState(
context.function == n,
"unexpected function context:\nexpected: %s\nactual: %s",
n,
context.function);
checkState(contextStack.removeFirst() == context);
if (context.isAsyncContext()) {
convertAsyncFunction(context);
}
Expand All @@ -172,15 +175,15 @@ public void visit(NodeTraversal t, Node n, Node parent) {
if (context.mustReplaceThisAndArguments() && n.matchesQualifiedName("arguments")) {
n.setString(ASYNC_ARGUMENTS);
context.recordAsyncArgumentsReplacementWasDone();
compiler.reportChangeToChangeScope(context.function.get());
compiler.reportChangeToChangeScope(context.function);
}
break;

case THIS:
if (context.mustReplaceThisAndArguments()) {
parent.replaceChild(n, IR.name(ASYNC_THIS).useSourceInfoIfMissingFrom(n));
parent.replaceChild(n, IR.name(ASYNC_THIS));
context.recordAsyncThisReplacementWasDone();
compiler.reportChangeToChangeScope(context.function.get());
compiler.reportChangeToChangeScope(context.function);
}
break;

Expand All @@ -198,23 +201,23 @@ public void visit(NodeTraversal t, Node n, Node parent) {
Node getPropReplacement = NodeUtil.newCallNode(IR.name(superPropertyName));
Node grandparent = parent.getParent();
if (grandparent.isCall() && grandparent.getFirstChild() == parent) {
// $super$get$x()(...) => $super$get$x().call($this, ...)
// super.x(...) => super.x.call($this, ...)
getPropReplacement = IR.getprop(getPropReplacement, IR.string("call"));
grandparent.addChildAfter(IR.name(ASYNC_THIS), parent);
grandparent.addChildAfter(IR.name(ASYNC_THIS).useSourceInfoFrom(parent), parent);
context.recordAsyncThisReplacementWasDone();
}
getPropReplacement.useSourceInfoFrom(parent);
getPropReplacement.useSourceInfoFromForTree(parent);
grandparent.replaceChild(parent, getPropReplacement);
context.recordAsyncSuperReplacementWasDone(medhodName.getString());
compiler.reportChangeToChangeScope(context.function.get());
compiler.reportChangeToChangeScope(context.function);
}
break;

case AWAIT:
checkState(context.isAsyncContext(), "await found within non-async function body");
checkState(n.hasOneChild(), "await should have 1 operand, but has %s", n.getChildCount());
// Awaits become yields in the converted async function's inner generator function.
parent.replaceChild(n, IR.yield(n.removeFirstChild()).useSourceInfoIfMissingFrom(n));
parent.replaceChild(n, IR.yield(n.removeFirstChild()));
break;

default:
Expand All @@ -223,7 +226,7 @@ public void visit(NodeTraversal t, Node n, Node parent) {
}

private void convertAsyncFunction(LexicalContext functionContext) {
Node originalFunction = functionContext.function.get();
Node originalFunction = checkNotNull(functionContext.function);
originalFunction.setIsAsyncFunction(false);
Node originalBody = originalFunction.getLastChild();
Node newBody = IR.block().useSourceInfoIfMissingFrom(originalBody);
Expand Down
36 changes: 29 additions & 7 deletions test/com/google/javascript/jscomp/RewriteAsyncFunctionsTest.java
Expand Up @@ -59,9 +59,9 @@ public void testInnerArrowFunctionUsingThis() {
lines(
"class X {",
" m() {",
" const $jscomp$async$this=this;",
" const $jscomp$async$this = this;",
" function* $jscomp$async$generator() {",
" return new Promise((resolve,reject)=>{",
" return new Promise((resolve, reject) => {",
" return $jscomp$async$this",
" });",
" }",
Expand Down Expand Up @@ -91,8 +91,8 @@ public void testInnerSuperCall() {
"}",
"class X extends A {",
" m() {",
" const $jscomp$async$this=this;",
" const $jscomp$async$super$get$m=()=>super.m;",
" const $jscomp$async$this = this;",
" const $jscomp$async$super$get$m = () => super.m;",
" function* $jscomp$async$generator() {",
" return $jscomp$async$super$get$m().call($jscomp$async$this);",
" }",
Expand Down Expand Up @@ -123,7 +123,7 @@ public void testInnerSuperReference() {
"}",
"class X extends A {",
" m() {",
" const $jscomp$async$super$get$m=()=>super.m;",
" const $jscomp$async$super$get$m = () => super.m;",
" function* $jscomp$async$generator() {",
" const tmp = $jscomp$async$super$get$m();",
" return tmp.call(null);",
Expand All @@ -133,6 +133,28 @@ public void testInnerSuperReference() {
"}"));
}

public void testNestedArrowFunctionUsingThis() {
test(
lines(
"class X {",
" m() {",
" return async () => (() => this);",
" }",
"}"),
lines(
"class X {",
" m() {",
" return () => {",
" const $jscomp$async$this = this;",
" function* $jscomp$async$generator() {",
" return () => $jscomp$async$this;",
" }",
" return $jscomp.executeAsyncGenerator($jscomp$async$generator())",
" }",
" }",
"}"));
}

public void testInnerArrowFunctionUsingArguments() {
test(
lines(
Expand All @@ -146,9 +168,9 @@ public void testInnerArrowFunctionUsingArguments() {
lines(
"class X {",
" m() {",
" const $jscomp$async$arguments=arguments;",
" const $jscomp$async$arguments = arguments;",
" function* $jscomp$async$generator() {",
" return new Promise((resolve,reject)=>{",
" return new Promise((resolve,reject) => {",
" return $jscomp$async$arguments",
" });",
" }",
Expand Down

0 comments on commit 8f8b4b0

Please sign in to comment.