All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.RewriteAsyncFunctions Maven / Gradle / Ivy

/*
 * Copyright 2016 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;

/**
 * Converts async functions to valid ES6 generator functions code.
 *
 * 

This pass must run before the passes that transpile let declarations, arrow functions, and * generators. * *

An async function, foo(a, b), will be rewritten as: * *

 {@code
 * function foo(a, b) {
 *   let $jscomp$async$this = this;
 *   let $jscomp$async$arguments = arguments;
 *   let $jscomp$async$super$get$x = () => super.x;
 *   return $jscomp.asyncExecutePromiseGeneratorFunction(
 *       function* () {
 *         // original body of foo() with:
 *         // - await (x) replaced with yield (x)
 *         // - arguments replaced with $jscomp$async$arguments
 *         // - this replaced with $jscomp$async$this
 *         // - super.x replaced with $jscomp$async$super$get$x()
 *         // - super.x(5) replaced with $jscomp$async$super$get$x().call($jscomp$async$this, 5)
 *       });
 * }}
*/ public final class RewriteAsyncFunctions implements NodeTraversal.Callback, HotSwapCompilerPass { private static final String ASYNC_ARGUMENTS = "$jscomp$async$arguments"; private static final String ASYNC_THIS = "$jscomp$async$this"; private static final String ASYNC_SUPER_PROP_GETTER_PREFIX = "$jscomp$async$super$get$"; /** * Information needed to replace a reference to `super.propertyName` with a call to a wrapper * function. */ private final class SuperPropertyWrapperInfo { // The first `super.property` Node we come across during traversal. // Information from this node will be used when creating the wrapper function. private final Node firstInstanceOfSuperDotProperty; private final String wrapperFunctionName; // The type to use for the wrapper function. // Will be null if type checking has not run. @Nullable private final JSType wrapperFunctionType; private SuperPropertyWrapperInfo( Node firstSuperDotPropertyNode, String wrapperFunctionName, JSType wrapperFunctionType) { this.firstInstanceOfSuperDotProperty = firstSuperDotPropertyNode; this.wrapperFunctionName = wrapperFunctionName; this.wrapperFunctionType = wrapperFunctionType; } @Nullable private JSType getPropertyType() { return firstInstanceOfSuperDotProperty.getJSType(); } private Node createWrapperFunctionNameNode() { return astFactory.createName(wrapperFunctionName, wrapperFunctionType); } private Node createWrapperFunctionCallNode() { return astFactory.createCall(createWrapperFunctionNameNode()); } } /** * Used to collect information about properties referenced via `super.propertyName` within an * async function. * *

We'll have to replace these references with calls to a wrapper function. */ private final class SuperPropertyWrappers { // Use LinkedHashMap in order to ensure the ordering is the same on every compile of the same // source code. // Note that the JSTypes will be null if type checking hasn't run. private final Map propertyNameToTypeMap = new LinkedHashMap<>(); private SuperPropertyWrapperInfo getOrCreateSuperPropertyWrapperInfo( Node superDotPropertyNode) { checkArgument(superDotPropertyNode.isGetProp(), superDotPropertyNode); Node superNode = superDotPropertyNode.getFirstChild(); checkArgument(superNode.isSuper(), superNode); Node propertyNameNode = superDotPropertyNode.getLastChild(); checkArgument(propertyNameNode.isString(), propertyNameNode); String propertyName = propertyNameNode.getString(); JSType propertyType = superDotPropertyNode.getJSType(); final SuperPropertyWrapperInfo superPropertyWrapperInfo; if (propertyNameToTypeMap.containsKey(propertyName)) { superPropertyWrapperInfo = propertyNameToTypeMap.get(propertyName); // Every reference to `super.propertyName` within a single lexical context should // have the same type. Make sure this is true. JSType existingJSType = superPropertyWrapperInfo.getPropertyType(); checkState( Objects.equals(existingJSType, propertyType), "Previous reference type: %s differs from current reference type: %s", existingJSType, propertyType); } else { superPropertyWrapperInfo = createNewInfo(superDotPropertyNode); propertyNameToTypeMap.put(propertyName, superPropertyWrapperInfo); } return superPropertyWrapperInfo; } private SuperPropertyWrapperInfo createNewInfo(Node firstSuperDotPropertyNode) { checkArgument(firstSuperDotPropertyNode.isGetProp(), firstSuperDotPropertyNode); String propertyName = firstSuperDotPropertyNode.getLastChild().getString(); JSType propertyType = firstSuperDotPropertyNode.getJSType(); final String wrapperFunctionName = ASYNC_SUPER_PROP_GETTER_PREFIX + propertyName; final JSType wrapperFunctionType; if (propertyType == null) { // type checking hasn't run, so we don't need type information. wrapperFunctionType = null; } else { wrapperFunctionType = FunctionType.builder(registry).withReturnType(propertyType).buildAndResolve(); } return new SuperPropertyWrapperInfo( firstSuperDotPropertyNode, wrapperFunctionName, wrapperFunctionType); } private Collection asCollection() { return propertyNameToTypeMap.values(); } } /** * Determines both what to do when visiting a node and how to determine the context for its * descendents. */ private abstract class LexicalContext { final Node contextRootNode; LexicalContext(Node contextRootNode) { this.contextRootNode = checkNotNull(contextRootNode); } Node getContextRootNode() { return contextRootNode; } /** * Returns the LexicalContext to use for visiting a node. * * @param n This context's root node or one of its descendents. * @return this context or a new one for a child context */ public abstract LexicalContext getContextForNode(Node n); public abstract void visit(NodeTraversal t, Node n); } /** Defines behavior for nodes in the root scope, outside of any functions. */ private final class RootContext extends LexicalContext { private RootContext(Node contextRootNode) { super(contextRootNode); } @Override public LexicalContext getContextForNode(Node n) { if (n.isFunction()) { return new FunctionContext(n); } else { return this; } } @Override public void visit(NodeTraversal t, Node n) { // In root context we haven't entered an async function yet, so there's nothing to do. } } /** Defines the behavior for function definition parameter lists and their contents. */ private final class ParameterListContext extends LexicalContext { final FunctionContext functionContext; public ParameterListContext(FunctionContext functionContext, Node contextRootNode) { super(contextRootNode); this.functionContext = checkNotNull(functionContext); } @Override public LexicalContext getContextForNode(Node n) { if (n.isFunction()) { // Function defined within a parameter list. // e.g. `() => something` // function someFunc(callback = () => something) {} return new FunctionContext(functionContext, n); } else { return this; } } @Override public void visit(NodeTraversal t, Node n) { if (functionContext.asyncThisAndArgumentsContext != null && functionContext.asyncThisAndArgumentsContext != functionContext) { // e.g. // async function outer(outerT = this) { // // `this` in outer parameter list must remain unchanged // // but inner parameter list must be aliased // const inner = async (t = this) => t; // } functionContext.visit(t, n); } } } /** * Defines behavior for replacing references to `this`, `arguments`, and `super` within async * functions and rewriting the async functions themselves. */ private final class FunctionContext extends LexicalContext { // If references to `this`, `arguments`, and `super` should be considered in the context of // an async function, this will point to that function's FunctionContext. // Otherwise, it will be `null`. // TODO(bradfordcsmith): It would cost less memory if we defined a separate object to hold // the data for async context accounting instead of having the booleans and super property // wrapper fields on every FunctionContext. @Nullable final FunctionContext asyncThisAndArgumentsContext; final SuperPropertyWrappers superPropertyWrappers = new SuperPropertyWrappers(); boolean mustAddAsyncThisVariable = false; boolean mustAddAsyncArgumentsVariable = false; FunctionContext(Node contextRootNode) { super(contextRootNode); if (contextRootNode.isAsyncFunction()) { asyncThisAndArgumentsContext = this; } else { asyncThisAndArgumentsContext = null; } } FunctionContext(FunctionContext outer, Node contextRootNode) { super(contextRootNode); checkState(contextRootNode.isFunction(), contextRootNode); if (contextRootNode.isAsyncFunction()) { if (contextRootNode.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 (contextRootNode.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; } } } @Override public LexicalContext getContextForNode(Node n) { if (n == contextRootNode) { return this; } else if (n.isFunction()) { return new FunctionContext(this, n); } else if (n.isParamList()) { return new ParameterListContext(this, n); } else { return this; } } private void recordAsyncThisReplacementWasDone() { asyncThisAndArgumentsContext.mustAddAsyncThisVariable = true; } private SuperPropertyWrapperInfo getOrCreateSuperPropertyWrapperInfo( Node superDotPropertyNode) { return asyncThisAndArgumentsContext.superPropertyWrappers.getOrCreateSuperPropertyWrapperInfo( superDotPropertyNode); } private void recordAsyncArgumentsReplacementWasDone() { asyncThisAndArgumentsContext.mustAddAsyncArgumentsVariable = true; } /** * Creates a new reference to the variable used to hold the value of `this` for async functions. */ private Node createThisVariableReference() { recordAsyncThisReplacementWasDone(); return astFactory.createThisAliasReferenceForFunction( ASYNC_THIS, asyncThisAndArgumentsContext.getContextRootNode()); } /** Creates a correctly typed `this` node for this context. */ private Node createThisReference() { return astFactory.createThisForFunction(asyncThisAndArgumentsContext.getContextRootNode()); } private Node createWrapperArrowFunction(SuperPropertyWrapperInfo wrapperInfo) { // super.propertyName final Node superDotProperty = wrapperInfo.firstInstanceOfSuperDotProperty.cloneTree(); if (rewriteSuperPropertyReferencesWithoutSuper) { // Rewrite to avoid using `super` within an arrow function. // See more information on definition of this option. // TODO(bradfordcsmith): RewriteAsyncIteration and RewriteAsyncFunctions have the // same logic for dealing with super references. Consider having them share // it from a common place instead of duplicating. final Node thisNode = createThisReference(); final Node prototypeOfThisNode = astFactory.createObjectGetPrototypeOfCall(thisNode); final Node originalSuperNode = superDotProperty.getFirstChild(); // NOTE: must look at the enclosing MEMBER_FUNCTION_DEF to see if the method is static if (asyncThisAndArgumentsContext.getContextRootNode().getParent().isStaticMember()) { // For static methods `this` is the class and its direct prototype is the parent // class and the super node we want // super.propertyName -> Object.getPrototypeOf(this).propertyName originalSuperNode.replaceWith(prototypeOfThisNode); } else { // For instance methods `this` is the instance, and its direct prototype is the // ClassName.prototype object. We must go to the prototype of that to get the correct // value for `super`. // super.propertyName -> Object.getPrototypeOf(Object.getPrototypeOf(this)).propertyName originalSuperNode.replaceWith( astFactory.createObjectGetPrototypeOfCall(prototypeOfThisNode)); } } // () => super.propertyName // OR avoid super for static method (class object -> superclass object) // () => Object.getPrototypeOf(this).x // OR avoid super for instance method (instance -> prototype -> super prototype) // () => Object.getPrototypeOf(Object.getPrototypeOf(this)).x return astFactory.createZeroArgArrowFunctionForExpression(superDotProperty); } @Override public void visit(NodeTraversal t, Node n) { if (contextRootNode == n && contextRootNode.isAsyncFunction()) { // We're visiting an async function. // All of its descendent nodes will have been updated as necessary, so now we just need to // convert the function itself. convertAsyncFunction(t, this); } else if (asyncThisAndArgumentsContext != null) { // We're in the context of an async function's body, so we need to do some replacements. switch (n.getToken()) { case NAME: if (n.matchesQualifiedName("arguments")) { n.setString(ASYNC_ARGUMENTS); asyncThisAndArgumentsContext.recordAsyncArgumentsReplacementWasDone(); compiler.reportChangeToChangeScope(contextRootNode); } break; case THIS: n.replaceWith(asyncThisAndArgumentsContext.createThisVariableReference()); compiler.reportChangeToChangeScope(contextRootNode); break; case SUPER: { Node parent = n.getParent(); if (!parent.isGetProp()) { compiler.report( JSError.make(parent, Es6ToEs3Util.CANNOT_CONVERT_YET, "super expression")); } // different name for parent for better readability Node superDotProperty = parent; SuperPropertyWrapperInfo superPropertyWrapperInfo = asyncThisAndArgumentsContext.getOrCreateSuperPropertyWrapperInfo( superDotProperty); // super.x => $jscomp$super$get$x() Node getPropReplacement = superPropertyWrapperInfo.createWrapperFunctionCallNode(); Node grandparent = superDotProperty.getParent(); if (grandparent.isCall() && grandparent.getFirstChild() == superDotProperty) { // $jscomp$super$get$x(...) => $jscomp$super$get$x().call($jscomp$async$this, // ...) getPropReplacement = astFactory.createGetProp(getPropReplacement, "call"); grandparent.addChildAfter( astFactory .createThisAliasReferenceForFunction( ASYNC_THIS, asyncThisAndArgumentsContext.getContextRootNode()) .useSourceInfoFrom(superDotProperty), superDotProperty); asyncThisAndArgumentsContext.recordAsyncThisReplacementWasDone(); } getPropReplacement.useSourceInfoFromForTree(superDotProperty); grandparent.replaceChild(superDotProperty, getPropReplacement); compiler.reportChangeToChangeScope(contextRootNode); } break; case AWAIT: // Awaits become yields in the converted async function's inner generator function. n.replaceWith(astFactory.createYield(n.getJSType(), n.removeFirstChild())); break; default: break; } } } } private static final FeatureSet transpiledFeatures = FeatureSet.BARE_MINIMUM.with(Feature.ASYNC_FUNCTIONS); private final Deque contextStack; private final AbstractCompiler compiler; /** * If this option is set to true, then this pass will rewrite references to properties using super * (e.g. `super.method()`) to avoid using `super` within an arrow function. * *

This option exists due to a bug in MS Edge 17 which causes it to fail to access super * properties correctly from within arrow functions. * *

See https://github.com/Microsoft/ChakraCore/issues/5784 * *

If the final compiler output will not include ES6 classes, this option should not be set. It * isn't needed since the `super` references will be transpiled away anyway. Also, when this * option is set it uses `Object.getPrototypeOf()` to rewrite `super`, which may not exist in * pre-ES6 JS environments. */ private final boolean rewriteSuperPropertyReferencesWithoutSuper; private final JSTypeRegistry registry; private final AstFactory astFactory; private RewriteAsyncFunctions(Builder builder) { checkNotNull(builder); this.compiler = builder.compiler; this.contextStack = new ArrayDeque<>(); this.rewriteSuperPropertyReferencesWithoutSuper = builder.rewriteSuperPropertyReferencesWithoutSuper; this.registry = checkNotNull(builder.registry); this.astFactory = checkNotNull(builder.astFactory); } static class Builder { private final AbstractCompiler compiler; private boolean rewriteSuperPropertyReferencesWithoutSuper = false; private JSTypeRegistry registry; private AstFactory astFactory; Builder(AbstractCompiler compiler) { checkNotNull(compiler); this.compiler = compiler; } Builder rewriteSuperPropertyReferencesWithoutSuper(boolean value) { rewriteSuperPropertyReferencesWithoutSuper = value; return this; } RewriteAsyncFunctions build() { astFactory = compiler.createAstFactory(); registry = compiler.getTypeRegistry(); return new RewriteAsyncFunctions(this); } } @Override public void process(Node externs, Node root) { TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, this); TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, this); TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, transpiledFeatures, this); TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures); } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (parent == null) { checkState(contextStack.isEmpty()); contextStack.push(new RootContext(n)); } else { LexicalContext parentContext = contextStack.peek(); LexicalContext nodeContext = parentContext.getContextForNode(n); if (nodeContext != parentContext) { contextStack.push(nodeContext); } } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { LexicalContext context = contextStack.peek(); context.visit(t, n); if (context.getContextRootNode() == n) { contextStack.pop(); } } private void convertAsyncFunction(NodeTraversal t, FunctionContext functionContext) { Node originalFunction = functionContext.getContextRootNode(); originalFunction.setIsAsyncFunction(false); Node originalBody = originalFunction.getLastChild(); if (originalFunction.isFromExterns()) { // A function defined in externs will never be executed, so we don't need to transpile it. // Make sure it has an empty body though so later passes won't trip over uses of `await` or // anything like that. if (!NodeUtil.isEmptyBlock(originalBody)) { // TODO(b/119685646): Maybe we should warn for non-empty functions in externs? originalBody.replaceWith(astFactory.createBlock()); NodeUtil.markFunctionsDeleted(originalBody, compiler); } return; } Node newBody = astFactory.createBlock(); originalFunction.replaceChild(originalBody, newBody); if (functionContext.mustAddAsyncThisVariable) { // const this$ = this; newBody.addChildToBack( astFactory.createThisAliasDeclarationForFunction(ASYNC_THIS, originalFunction)); NodeUtil.addFeatureToScript(t.getCurrentScript(), Feature.CONST_DECLARATIONS, compiler); } if (functionContext.mustAddAsyncArgumentsVariable) { // const arguments$ = arguments; newBody.addChildToBack(astFactory.createArgumentsAliasDeclaration(ASYNC_ARGUMENTS)); NodeUtil.addFeatureToScript(t.getCurrentScript(), Feature.CONST_DECLARATIONS, compiler); } for (SuperPropertyWrapperInfo superPropertyWrapperInfo : functionContext.superPropertyWrappers.asCollection()) { Node arrowFunction = functionContext.createWrapperArrowFunction(superPropertyWrapperInfo); // const super$get$x = () => super.x; Node arrowFunctionDeclarationStatement = astFactory.createSingleConstNameDeclaration( superPropertyWrapperInfo.wrapperFunctionName, arrowFunction); newBody.addChildToBack(arrowFunctionDeclarationStatement); // Make sure the compiler knows about the new arrow function's scope compiler.reportChangeToChangeScope(arrowFunction); // Record that we've added arrow functions and const declarations to this script, // so later transpilations of those features will run, if needed. Node enclosingScript = t.getCurrentScript(); NodeUtil.addFeatureToScript(enclosingScript, Feature.ARROW_FUNCTIONS, compiler); NodeUtil.addFeatureToScript(enclosingScript, Feature.CONST_DECLARATIONS, compiler); } // Normalize arrow function short body to block body if (!originalBody.isBlock()) { originalBody = astFactory .createBlock(astFactory.createReturn(originalBody)) .useSourceInfoIfMissingFromForTree(originalBody); } // NOTE: visit() will already have made appropriate replacements in originalBody so it may // be used as the generator function body. Node generatorFunction = astFactory.createZeroArgGeneratorFunction("", originalBody, originalFunction.getJSType()); compiler.reportChangeToChangeScope(generatorFunction); NodeUtil.addFeatureToScript(t.getCurrentScript(), Feature.GENERATORS, compiler); // return $jscomp.asyncExecutePromiseGeneratorFunction(function* () { ... }); newBody.addChildToBack( astFactory.createReturn( astFactory.createJscompAsyncExecutePromiseGeneratorFunctionCall( t.getScope(), generatorFunction))); newBody.useSourceInfoIfMissingFromForTree(originalBody); compiler.reportChangeToEnclosingScope(newBody); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy