From d01e47e2214af56e514f7b99f5d492e3461f5b00 Mon Sep 17 00:00:00 2001 From: bradfordcsmith Date: Thu, 17 May 2018 09:52:32 -0700 Subject: [PATCH] Split EarlyEs6ToEs3converter into 2 passes Es6InjectRuntimeLibraries is injects runtime code needed for Symbol, Symbol.iterator, and for-of transpilation. Es6RewriteRestAndSpread transpiles REST parameters and SPREAD expressions. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197006873 --- .../jscomp/Es6InjectRuntimeLibraries.java | 136 ++++++++++++++ .../jscomp/Es6RewriteGenerators.java | 2 +- ...rter.java => Es6RewriteRestAndSpread.java} | 175 +++++------------- .../jscomp/TranspilationPasses.java | 45 +++-- .../jscomp/Es6ToEs3ConverterTest.java | 15 +- 5 files changed, 215 insertions(+), 158 deletions(-) create mode 100644 src/com/google/javascript/jscomp/Es6InjectRuntimeLibraries.java rename src/com/google/javascript/jscomp/{EarlyEs6ToEs3Converter.java => Es6RewriteRestAndSpread.java} (71%) diff --git a/src/com/google/javascript/jscomp/Es6InjectRuntimeLibraries.java b/src/com/google/javascript/jscomp/Es6InjectRuntimeLibraries.java new file mode 100644 index 00000000000..bafd1d1fb93 --- /dev/null +++ b/src/com/google/javascript/jscomp/Es6InjectRuntimeLibraries.java @@ -0,0 +1,136 @@ +/* + * Copyright 2014 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 com.google.javascript.jscomp.CompilerOptions.LanguageMode; +import com.google.javascript.jscomp.NodeTraversal.Callback; +import com.google.javascript.jscomp.parsing.parser.FeatureSet; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; + +/** + * Injects JS library code that may be needed by the transpiled form of the input source code. + * + *

The intention here is to add anything that could be needed and rely on `RemoveUnusedCode` to + * remove the parts that don't end up getting used. This pass should run before type checking so the + * type checking code can add type information to the injected JavaScript for checking and + * optimization purposes. + * + * This class also reports an error if it finds getters or setters are used and the language output + * level is too low to support them. + * TODO(bradfordcsmith): The getter/setter check should probably be done separately in an earlier + * pass that only runs when the output language level is ES3 and the input language level is + * ES5 or greater. + */ +public final class Es6InjectRuntimeLibraries implements Callback, HotSwapCompilerPass { + private final AbstractCompiler compiler; + + // Since there's currently no Feature for Symbol, run this pass if the code has any ES6 features. + private static final FeatureSet requiredForFeatures = FeatureSet.ES6.without(FeatureSet.ES5); + + public Es6InjectRuntimeLibraries(AbstractCompiler compiler) { + this.compiler = compiler; + } + + @Override + public void process(Node externs, Node root) { + TranspilationPasses.processTranspile(compiler, root, requiredForFeatures, this); + } + + @Override + public void hotSwapScript(Node scriptRoot, Node originalRoot) { + TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, requiredForFeatures, this); + } + + @Override + public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { + switch (n.getToken()) { + case FOR_OF: + // We will need this when we transpile for/of in LateEs6ToEs3Converter, + // but we want the runtime functions to be have JSType applied to it by the type checker. + Es6ToEs3Util.preloadEs6RuntimeFunction(compiler, "makeIterator"); + break; + case GETTER_DEF: + case SETTER_DEF: + if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) { + Es6ToEs3Util.cannotConvert( + compiler, n, "ES5 getters/setters (consider using --language_out=ES5)"); + } + break; + case FUNCTION: + if (n.isAsyncFunction()) { + throw new IllegalStateException("async functions should have already been converted"); + } + if (n.isGeneratorFunction()) { + compiler.ensureLibraryInjected("es6/generator_engine", /* force= */ false); + } + break; + default: + break; + } + return true; + } + + @Override + public void visit(NodeTraversal t, Node n, Node parent) { + switch (n.getToken()) { + case NAME: + if (!n.isFromExterns() && isGlobalSymbol(t, n)) { + initSymbolBefore(n); + } + break; + case GETPROP: + if (!n.isFromExterns()) { + visitGetprop(t, n); + } + break; + default: + break; + } + } + + /** @return Whether {@code n} is a reference to the global "Symbol" function. */ + private boolean isGlobalSymbol(NodeTraversal t, Node n) { + if (!n.matchesQualifiedName("Symbol")) { + return false; + } + Var var = t.getScope().getVar("Symbol"); + return var == null || var.isGlobal(); + } + + /** Inserts a call to $jscomp.initSymbol() before {@code n}. */ + private void initSymbolBefore(Node n) { + compiler.ensureLibraryInjected("es6/symbol", false); + Node statement = NodeUtil.getEnclosingStatement(n); + Node initSymbol = IR.exprResult(IR.call(NodeUtil.newQName(compiler, "$jscomp.initSymbol"))); + statement.getParent().addChildBefore(initSymbol.useSourceInfoFromForTree(statement), statement); + compiler.reportChangeToEnclosingScope(initSymbol); + } + + // TODO(tbreisacher): Do this for all well-known symbols. + private void visitGetprop(NodeTraversal t, Node n) { + if (!n.matchesQualifiedName("Symbol.iterator")) { + return; + } + if (isGlobalSymbol(t, n.getFirstChild())) { + compiler.ensureLibraryInjected("es6/symbol", false); + Node statement = NodeUtil.getEnclosingStatement(n); + Node init = IR.exprResult(IR.call(NodeUtil.newQName(compiler, "$jscomp.initSymbolIterator"))); + statement.getParent().addChildBefore(init.useSourceInfoFromForTree(statement), statement); + compiler.reportChangeToEnclosingScope(init); + } + } +} diff --git a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java index b9734641fe1..50f3dda1712 100644 --- a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java +++ b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java @@ -62,7 +62,7 @@ * * * - *

{@code Es6RewriteGenerators} depends on {@link EarlyEs6ToEs3Converter} to inject + *

{@code Es6RewriteGenerators} depends on {@link Es6InjectRuntimeLibraries} to inject * generator_engine.js template. */ final class Es6RewriteGenerators implements HotSwapCompilerPass { diff --git a/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java b/src/com/google/javascript/jscomp/Es6RewriteRestAndSpread.java similarity index 71% rename from src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java rename to src/com/google/javascript/jscomp/Es6RewriteRestAndSpread.java index 648cecf88f0..9077df469ed 100644 --- a/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java +++ b/src/com/google/javascript/jscomp/Es6RewriteRestAndSpread.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; -import com.google.javascript.jscomp.NodeTraversal.Callback; import com.google.javascript.jscomp.NodeUtil.Visitor; import com.google.javascript.jscomp.parsing.parser.FeatureSet; import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature; @@ -32,23 +31,15 @@ import java.util.ArrayList; import java.util.List; -/** - * Converts ES6 code to valid ES5 code. This class does transpilation for Rest, Spread, and Symbols, - * which should be transpiled before NTI. - * Other classes that start with "Es6" do other parts of the transpilation. - * - *

In most cases, the output is valid as ES3 (hence the class name) but in some cases, if - * the output language is set to ES5, we rely on ES5 features such as getters, setters, - * and Object.defineProperties. - * - * @author tbreisacher@google.com (Tyler Breisacher) - */ -public final class EarlyEs6ToEs3Converter implements Callback, HotSwapCompilerPass { +/** Converts REST parameters and SPREAD expressions. */ +public final class Es6RewriteRestAndSpread extends NodeTraversal.AbstractPostOrderCallback + implements HotSwapCompilerPass { private final AbstractCompiler compiler; - static final DiagnosticType BAD_REST_PARAMETER_ANNOTATION = DiagnosticType.warning( - "BAD_REST_PARAMETER_ANNOTATION", - "Missing \"...\" in type annotation for rest parameter."); + static final DiagnosticType BAD_REST_PARAMETER_ANNOTATION = + DiagnosticType.warning( + "BAD_REST_PARAMETER_ANNOTATION", + "Missing \"...\" in type annotation for rest parameter."); // The name of the index variable for populating the rest parameter array. private static final String REST_INDEX = "$jscomp$restIndex"; @@ -57,81 +48,32 @@ public final class EarlyEs6ToEs3Converter implements Callback, HotSwapCompilerPa private static final String REST_PARAMS = "$jscomp$restParams"; private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args"; - // Since there's currently no Feature for Symbol, run this pass if the code has any ES6 features. - private static final FeatureSet requiredForFeatures = FeatureSet.ES6.without(FeatureSet.ES5); - private static final FeatureSet featuresTranspiledAway = - FeatureSet.BARE_MINIMUM.with( - Feature.REST_PARAMETERS, - Feature.SPREAD_EXPRESSIONS); + private static final FeatureSet transpiledFeatures = + FeatureSet.BARE_MINIMUM.with(Feature.REST_PARAMETERS, Feature.SPREAD_EXPRESSIONS); - public EarlyEs6ToEs3Converter(AbstractCompiler compiler) { + public Es6RewriteRestAndSpread(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { - TranspilationPasses.processTranspile(compiler, externs, requiredForFeatures, this); - TranspilationPasses.processTranspile(compiler, root, requiredForFeatures, this); - TranspilationPasses.markFeaturesAsTranspiledAway(compiler, featuresTranspiledAway); + TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, this); + TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, this); + TranspilationPasses.markFeaturesAsTranspiledAway(compiler, transpiledFeatures); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { - TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, requiredForFeatures, this); - TranspilationPasses.markFeaturesAsTranspiledAway(compiler, featuresTranspiledAway); + TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, transpiledFeatures, this); + TranspilationPasses.markFeaturesAsTranspiledAway(compiler, transpiledFeatures); } - /** - * Some nodes must be visited pre-order in order to rewrite the - * references to {@code this} correctly. - * Everything else is translated post-order in {@link #visit}. - */ @Override - public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { + public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case REST: visitRestParam(t, n, parent); break; - case FOR_OF: - // We will need this when we transpile for/of in LateEs6ToEs3Converter, - // but we want the runtime functions to be have JSType applied to it by the type checker. - Es6ToEs3Util.preloadEs6RuntimeFunction(compiler, "makeIterator"); - break; - case GETTER_DEF: - case SETTER_DEF: - if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) { - Es6ToEs3Util.cannotConvert( - compiler, n, "ES5 getters/setters (consider using --language_out=ES5)"); - return false; - } - break; - case FUNCTION: - if (n.isAsyncFunction()) { - throw new IllegalStateException("async functions should have already been converted"); - } - if (n.isGeneratorFunction()) { - compiler.ensureLibraryInjected("es6/generator_engine", /* force= */ false); - } - break; - default: - break; - } - return true; - } - - @Override - public void visit(NodeTraversal t, Node n, Node parent) { - switch (n.getToken()) { - case NAME: - if (!n.isFromExterns() && isGlobalSymbol(t, n)) { - initSymbolBefore(n); - } - break; - case GETPROP: - if (!n.isFromExterns()) { - visitGetprop(t, n); - } - break; case ARRAYLIT: case NEW: case CALL: @@ -147,45 +89,7 @@ public void visit(NodeTraversal t, Node n, Node parent) { } } - /** - * @return Whether {@code n} is a reference to the global "Symbol" function. - */ - private boolean isGlobalSymbol(NodeTraversal t, Node n) { - if (!n.matchesQualifiedName("Symbol")) { - return false; - } - Var var = t.getScope().getVar("Symbol"); - return var == null || var.isGlobal(); - } - - /** - * Inserts a call to $jscomp.initSymbol() before {@code n}. - */ - private void initSymbolBefore(Node n) { - compiler.ensureLibraryInjected("es6/symbol", false); - Node statement = NodeUtil.getEnclosingStatement(n); - Node initSymbol = IR.exprResult(IR.call(NodeUtil.newQName(compiler, "$jscomp.initSymbol"))); - statement.getParent().addChildBefore(initSymbol.useSourceInfoFromForTree(statement), statement); - compiler.reportChangeToEnclosingScope(initSymbol); - } - - // TODO(tbreisacher): Do this for all well-known symbols. - private void visitGetprop(NodeTraversal t, Node n) { - if (!n.matchesQualifiedName("Symbol.iterator")) { - return; - } - if (isGlobalSymbol(t, n.getFirstChild())) { - compiler.ensureLibraryInjected("es6/symbol", false); - Node statement = NodeUtil.getEnclosingStatement(n); - Node init = IR.exprResult(IR.call(NodeUtil.newQName(compiler, "$jscomp.initSymbolIterator"))); - statement.getParent().addChildBefore(init.useSourceInfoFromForTree(statement), statement); - compiler.reportChangeToEnclosingScope(init); - } - } - - /** - * Processes a rest parameter - */ + /** Processes a rest parameter */ private void visitRestParam(NodeTraversal t, Node restParam, Node paramList) { Node functionBody = paramList.getNext(); int restIndex = paramList.getIndexOfChild(restParam); @@ -219,8 +123,7 @@ private void visitRestParam(NodeTraversal t, Node restParam, Node paramList) { Node newBlock = IR.block().useSourceInfoFrom(functionBody); Node name = IR.name(paramName); - Node let = IR.let(name, IR.name(REST_PARAMS)) - .useSourceInfoIfMissingFromForTree(functionBody); + Node let = IR.let(name, IR.name(REST_PARAMS)).useSourceInfoIfMissingFromForTree(functionBody); newBlock.addChildToFront(let); for (Node child : functionBody.children()) { @@ -251,11 +154,15 @@ private void visitRestParam(NodeTraversal t, Node restParam, Node paramList) { Node init = IR.var(IR.name(REST_INDEX), IR.number(restIndex)); Node cond = IR.lt(IR.name(REST_INDEX), IR.getprop(IR.name("arguments"), IR.string("length"))); Node incr = IR.inc(IR.name(REST_INDEX), false); - Node body = IR.block(IR.exprResult(IR.assign( - IR.getelem(IR.name(REST_PARAMS), IR.sub(IR.name(REST_INDEX), IR.number(restIndex))), - IR.getelem(IR.name("arguments"), IR.name(REST_INDEX))))); - functionBody.addChildAfter(IR.forNode(init, cond, incr, body) - .useSourceInfoIfMissingFromForTree(restParam), newArr); + Node body = + IR.block( + IR.exprResult( + IR.assign( + IR.getelem( + IR.name(REST_PARAMS), IR.sub(IR.name(REST_INDEX), IR.number(restIndex))), + IR.getelem(IR.name("arguments"), IR.name(REST_INDEX))))); + functionBody.addChildAfter( + IR.forNode(init, cond, incr, body).useSourceInfoIfMissingFromForTree(restParam), newArr); functionBody.addChildToBack(newBlock); compiler.reportChangeToEnclosingScope(newBlock); @@ -270,25 +177,27 @@ private Node replaceTypeVariablesWithUnknown(JSDocInfo functionJsdoc, Node typeA if (typeVars.isEmpty()) { return typeAst; } - NodeUtil.visitPreOrder(typeAst, new Visitor(){ - @Override - public void visit(Node n) { - if (n.isString() && n.getParent() != null && typeVars.contains(n.getString())) { - n.replaceWith(new Node(Token.QMARK)); - } - } - }); + NodeUtil.visitPreOrder( + typeAst, + new Visitor() { + @Override + public void visit(Node n) { + if (n.isString() && n.getParent() != null && typeVars.contains(n.getString())) { + n.replaceWith(new Node(Token.QMARK)); + } + } + }); return typeAst; } /** - * Processes array literals or calls containing spreads. Examples: - * [1, 2, ...x, 4, 5] => [].concat([1, 2], $jscomp.arrayFromIterable(x), [4, 5]) + * Processes array literals or calls containing spreads. Examples: [1, 2, ...x, 4, 5] => + * [].concat([1, 2], $jscomp.arrayFromIterable(x), [4, 5]) * - * f(...arr) => f.apply(null, [].concat($jscomp.arrayFromIterable(arr))) + *

f(...arr) => f.apply(null, [].concat($jscomp.arrayFromIterable(arr))) * - * new F(...args) => - * new Function.prototype.bind.apply(F, [].concat($jscomp.arrayFromIterable(args))) + *

new F(...args) => new Function.prototype.bind.apply(F, + * [].concat($jscomp.arrayFromIterable(args))) */ private void visitArrayLitOrCallWithSpread(Node node, Node parent) { if (node.isArrayLit()) { diff --git a/src/com/google/javascript/jscomp/TranspilationPasses.java b/src/com/google/javascript/jscomp/TranspilationPasses.java index 54bb2cd181e..6338d836eeb 100644 --- a/src/com/google/javascript/jscomp/TranspilationPasses.java +++ b/src/com/google/javascript/jscomp/TranspilationPasses.java @@ -95,7 +95,8 @@ public static void addEs6PreTypecheckPasses(List passes, CompilerOp passes.add(es6RewriteArrowFunction); passes.add(es6ExtractClasses); passes.add(es6RewriteClass); - passes.add(earlyConvertEs6ToEs3); + passes.add(es6InjectRuntimeLibraries); + passes.add(es6RewriteRestAndSpread); passes.add(lateConvertEs6ToEs3); if (!options.checksOnly) { // Don't run these passes in checksOnly mode since all the typechecking & checks passes @@ -294,23 +295,33 @@ protected FeatureSet featureSet() { } }; - /** - * Does ES6 to ES3 conversion of Rest, Spread and Symbol. - * There are a few other passes which run before or after this one, - * to convert constructs which are not converted by this pass. - */ - static final HotSwapPassFactory earlyConvertEs6ToEs3 = - new HotSwapPassFactory("earlyConvertEs6") { - @Override - protected HotSwapCompilerPass create(final AbstractCompiler compiler) { - return new EarlyEs6ToEs3Converter(compiler); - } + /** Injects runtime library code needed for transpiled ES6 code. */ + static final HotSwapPassFactory es6InjectRuntimeLibraries = + new HotSwapPassFactory("es6InjectRuntimeLibraries") { + @Override + protected HotSwapCompilerPass create(final AbstractCompiler compiler) { + return new Es6InjectRuntimeLibraries(compiler); + } - @Override - protected FeatureSet featureSet() { - return ES8; - } - }; + @Override + protected FeatureSet featureSet() { + return ES8; + } + }; + + /** Transpiles REST parameters and SPREAD in both array literals and function calls. */ + static final HotSwapPassFactory es6RewriteRestAndSpread = + new HotSwapPassFactory("es6RewriteRestAndSpread") { + @Override + protected HotSwapCompilerPass create(final AbstractCompiler compiler) { + return new Es6RewriteRestAndSpread(compiler); + } + + @Override + protected FeatureSet featureSet() { + return ES8; + } + }; /** * Does the main ES6 to ES3 conversion. There are a few other passes which run before this one, to diff --git a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java index 1848e7375a8..6bedc4f5448 100644 --- a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java +++ b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java @@ -28,11 +28,10 @@ import com.google.javascript.jscomp.parsing.parser.FeatureSet; /** - * Test cases for ES6 transpilation. Despite the name, this isn't just testing {@link - * EarlyEs6ToEs3Converter} and {@link LateEs6ToEs3Converter}, - * but also some other ES6 transpilation passes. See {@link #getProcessor}. + * Test cases for ES6 transpilation. * - * @author tbreisacher@google.com (Tyler Breisacher) + * This class actually tests several transpilation passes together. + * See {@link #getProcessor}. */ // TODO(tbreisacher): Rename this to Es6TranspilationIntegrationTest since it's really testing // a lot of different passes. Also create a unit test for Es6ToEs3Converter. @@ -131,7 +130,9 @@ protected CompilerPass getProcessor(final Compiler compiler) { optimizer.addOneTimePass(makePassFactory("es6ExtractClasses", new Es6ExtractClasses(compiler))); optimizer.addOneTimePass(makePassFactory("es6RewriteClass", new Es6RewriteClass(compiler))); optimizer.addOneTimePass( - makePassFactory("convertEs6Early", new EarlyEs6ToEs3Converter(compiler))); + makePassFactory("es6InjectRuntimeLibraries", new Es6InjectRuntimeLibraries(compiler))); + optimizer.addOneTimePass( + makePassFactory("es6RewriteRestAndSpread", new Es6RewriteRestAndSpread(compiler))); optimizer.addOneTimePass( makePassFactory("convertEs6Late", new LateEs6ToEs3Converter(compiler))); optimizer.addOneTimePass(makePassFactory("es6ForOf", new Es6ForOfConverter(compiler))); @@ -2286,9 +2287,9 @@ public void testRestParameter() { // Warn on /** number */ testWarning("function f(/** number */ ...zero) {}", - EarlyEs6ToEs3Converter.BAD_REST_PARAMETER_ANNOTATION); + Es6RewriteRestAndSpread.BAD_REST_PARAMETER_ANNOTATION); testWarning("/** @param {number} zero */ function f(...zero) {}", - EarlyEs6ToEs3Converter.BAD_REST_PARAMETER_ANNOTATION); + Es6RewriteRestAndSpread.BAD_REST_PARAMETER_ANNOTATION); } public void testDefaultAndRestParameters() {