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

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

/*
 * 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 static com.google.common.base.Preconditions.checkArgument;
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;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
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 [email protected] (Tyler Breisacher) */ public final class EarlyEs6ToEs3Converter implements Callback, 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."); // The name of the index variable for populating the rest parameter array. private static final String REST_INDEX = "$jscomp$restIndex"; // The name of the placeholder for the rest parameters. 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.ARRAY_PATTERN_REST, Feature.REST_PARAMETERS, Feature.SPREAD_EXPRESSIONS); public EarlyEs6ToEs3Converter(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); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, requiredForFeatures, this); TranspilationPasses.markFeaturesAsTranspiledAway(compiler, featuresTranspiledAway); } /** * 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) { 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 TypeI 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: for (Node child : n.children()) { if (child.isSpread()) { visitArrayLitOrCallWithSpread(n, parent); break; } } 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); } } /** * Processes a rest parameter */ private void visitRestParam(NodeTraversal t, Node restParam, Node paramList) { Node functionBody = paramList.getNext(); int restIndex = paramList.getIndexOfChild(restParam); String paramName = restParam.getFirstChild().getString(); Node nameNode = IR.name(paramName); nameNode.setVarArgs(true); nameNode.setJSDocInfo(restParam.getJSDocInfo()); paramList.replaceChild(restParam, nameNode); // Make sure rest parameters are typechecked JSTypeExpression type = null; JSDocInfo info = restParam.getJSDocInfo(); JSDocInfo functionInfo = NodeUtil.getBestJSDocInfo(paramList.getParent()); if (info != null) { type = info.getType(); } else { if (functionInfo != null) { type = functionInfo.getParameterType(paramName); } } if (type != null && type.getRoot().getToken() != Token.ELLIPSIS) { compiler.report(JSError.make(restParam, BAD_REST_PARAMETER_ANNOTATION)); } if (!functionBody.hasChildren()) { // If function has no body, we are done! t.reportCodeChange(); return; } Node newBlock = IR.block().useSourceInfoFrom(functionBody); Node name = IR.name(paramName); Node let = IR.let(name, IR.name(REST_PARAMS)) .useSourceInfoIfMissingFromForTree(functionBody); newBlock.addChildToFront(let); for (Node child : functionBody.children()) { newBlock.addChildToBack(child.detach()); } if (type != null) { Node arrayType = IR.string("Array"); Node typeNode = type.getRoot(); Node memberType = typeNode.getToken() == Token.ELLIPSIS ? typeNode.getFirstChild().cloneTree() : typeNode.cloneTree(); if (functionInfo != null) { memberType = replaceTypeVariablesWithUnknown(functionInfo, memberType); } arrayType.addChildToFront( new Node(Token.BLOCK, memberType).useSourceInfoIfMissingFrom(typeNode)); JSDocInfoBuilder builder = new JSDocInfoBuilder(false); builder.recordType( new JSTypeExpression(new Node(Token.BANG, arrayType), restParam.getSourceFileName())); name.setJSDocInfo(builder.build()); } // TODO(b/74074478): Use a general utility method instead of an inlined loop. Node newArr = IR.var(IR.name(REST_PARAMS), IR.arraylit()); functionBody.addChildToFront(newArr.useSourceInfoIfMissingFromForTree(restParam)); 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); functionBody.addChildToBack(newBlock); compiler.reportChangeToEnclosingScope(newBlock); // For now, we are running transpilation before type-checking, so we'll // need to make sure changes don't invalidate the JSDoc annotations. // Therefore we keep the parameter list the same length and only initialize // the values if they are set to undefined. } private Node replaceTypeVariablesWithUnknown(JSDocInfo functionJsdoc, Node typeAst) { final List typeVars = functionJsdoc.getTemplateTypeNames(); 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)); } } }); return typeAst; } /** * 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))) * * new F(...args) => * new Function.prototype.bind.apply(F, [].concat($jscomp.arrayFromIterable(args))) */ private void visitArrayLitOrCallWithSpread(Node node, Node parent) { if (node.isArrayLit()) { visitArrayLitWithSpread(node, parent); } else if (node.isCall()) { visitCallWithSpread(node, parent); } else { checkArgument(node.isNew(), node); visitNewWithSpread(node, parent); } } /** * Extracts child nodes from an array literal, call or new node that may contain spread operators * into a list of nodes that may be concatenated with Array.concat() to get an array. * *

Example: [a, b, ...x, c, ...arguments] returns a list containing [ [a, b], * $jscomp.arrayFromIterable(x), [c], $jscomp.arrayFromIterable(arguments) ] * *

IMPORTANT: Call and New nodes must have the first, callee, child removed already. * *

Note that all elements of the returned list will be one of: * *

    *
  • array literal *
  • $jscomp.arrayFromIterable(spreadExpression) *
* * TODO(bradfordcsmith): When this pass moves after type checking, we can use type information to * avoid unnecessary calls to $jscomp.arrayFromIterable(). */ private List extractSpreadGroups(Node parentNode) { checkArgument(parentNode.isCall() || parentNode.isArrayLit() || parentNode.isNew()); List groups = new ArrayList<>(); Node currGroup = null; Node currElement = parentNode.removeFirstChild(); while (currElement != null) { if (currElement.isSpread()) { Node spreadExpression = currElement.removeFirstChild(); if (spreadExpression.isArrayLit()) { // We can expand an array literal spread in place. if (currGroup == null) { // [...[spread, contents], a, b] // we can use this array lit itself as a group and append following elements to it currGroup = spreadExpression; } else { // [ a, b, ...[spread, contents], c] // Just add contents of this array lit to the group we were already collecting. currGroup.addChildrenToBack(spreadExpression.removeChildren()); } } else { // We need to treat the spread expression as a separate group if (currGroup != null) { // finish off and add the group we were collecting before groups.add(currGroup); currGroup = null; } groups.add(Es6ToEs3Util.arrayFromIterable(compiler, spreadExpression)); } } else { if (currGroup == null) { currGroup = IR.arraylit(); } currGroup.addChildToBack(currElement); } currElement = parentNode.removeFirstChild(); } if (currGroup != null) { groups.add(currGroup); } return groups; } /** * Processes array literals containing spreads. * *

Example: * *


   * [1, 2, ...x, 4, 5] => [1, 2].concat($jscomp.arrayFromIterable(x), [4, 5])
   * 
*/ private void visitArrayLitWithSpread(Node node, Node parent) { checkArgument(node.isArrayLit()); List groups = extractSpreadGroups(node); Node baseArrayLit; if (groups.get(0).isArrayLit()) { baseArrayLit = groups.remove(0); } else { baseArrayLit = IR.arraylit(); // [].concat(g0, g1, g2, ..., gn) } Node joinedGroups = groups.isEmpty() ? baseArrayLit : IR.call(IR.getprop(baseArrayLit, IR.string("concat")), groups.toArray(new Node[0])); joinedGroups.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, joinedGroups); compiler.reportChangeToEnclosingScope(joinedGroups); } /** * Processes calls containing spreads. * *

Examples: * *


   * f(...arr) => f.apply(null, $jscomp.arrayFromIterable(arr))
   * f(a, ...arr) => f.apply(null, [a].concat($jscomp.arrayFromIterable(arr)))
   * f(...arr, b) => f.apply(null, [].concat($jscomp.arrayFromIterable(arr), [b]))
   * 
*/ private void visitCallWithSpread(Node node, Node parent) { checkArgument(node.isCall()); // must remove callee before extracting argument groups Node callee = node.removeFirstChild(); Node joinedGroups; if (node.hasOneChild() && isSpreadOfArguments(node.getOnlyChild())) { // Check for special case of // `foo(...arguments)` and pass `arguments` directly to `foo.apply(null, arguments)`. // We want to avoid calling $jscomp.arrayFromIterable(arguments) for this case, // because it can have side effects, which prevents code removal. // TODO(b/74074478): generalize this to avoid ever calling $jscomp.arrayFromIterable() for // `arguments`. joinedGroups = node.removeFirstChild().removeFirstChild(); } else { List groups = extractSpreadGroups(node); checkState(!groups.isEmpty()); if (groups.size() == 1) { // single group can just be passed to apply() as-is // It could be `arguments`, an array literal, or $jscomp.arrayFromIterable(someExpression). joinedGroups = groups.remove(0); } else { // If the first group is an array literal, we can just use that for concatenation, // otherwise use an empty array literal. Node baseArrayLit = groups.get(0).isArrayLit() ? groups.remove(0) : IR.arraylit(); joinedGroups = groups.isEmpty() ? baseArrayLit : IR.call( IR.getprop(baseArrayLit, IR.string("concat")), groups.toArray(new Node[0])); } } Node result = null; if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) { // foo().method(...[a, b, c]) // must convert to // var freshVar; // (freshVar = foo()).method.apply(freshVar, [a, b, c]) Node statement = node; while (!NodeUtil.isStatement(statement)) { statement = statement.getParent(); } Node freshVar = IR.name(FRESH_SPREAD_VAR + compiler.getUniqueNameIdSupplier().get()); Node n = IR.var(freshVar.cloneTree()); n.useSourceInfoIfMissingFromForTree(statement); statement.getParent().addChildBefore(n, statement); callee.addChildToFront(IR.assign(freshVar.cloneTree(), callee.removeFirstChild())); result = IR.call(IR.getprop(callee, IR.string("apply")), freshVar, joinedGroups); } else { // foo.method(...[a, b, c]) -> foo.method.apply(foo, [a, b, c] // or // foo(...[a, b, c]) -> foo.apply(null, [a, b, c]) Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode(); result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups); } result.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, result); compiler.reportChangeToEnclosingScope(result); } private boolean isSpreadOfArguments(Node n) { return n.isSpread() && n.getOnlyChild().matchesQualifiedName("arguments"); } /** * Processes new calls containing spreads. * *

Example: * *


   * new F(...args) =>
   *     new Function.prototype.bind.apply(F, [].concat($jscomp.arrayFromIterable(args)))
   * 
*/ private void visitNewWithSpread(Node node, Node parent) { checkArgument(node.isNew()); // must remove callee before extracting argument groups Node callee = node.removeFirstChild(); List groups = extractSpreadGroups(node); // We need to generate // new (Function.prototype.bind.apply(callee, [null].concat(other, args))(); // null stands in for the 'this' arg to bind Node baseArrayLit; if (groups.get(0).isArrayLit()) { baseArrayLit = groups.remove(0); } else { baseArrayLit = IR.arraylit(); } baseArrayLit.addChildToFront(IR.nullNode()); Node joinedGroups = groups.isEmpty() ? baseArrayLit : IR.call(IR.getprop(baseArrayLit, IR.string("concat")), groups.toArray(new Node[0])); if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) { // TODO(tbreisacher): Support this in ES3 too by not relying on Function.bind. Es6ToEs3Util.cannotConvert( compiler, node, "\"...\" passed to a constructor (consider using --language_out=ES5)"); } Node bindApply = NodeUtil.newQName(compiler, "Function.prototype.bind.apply"); Node result = IR.newNode(IR.call(bindApply, callee, joinedGroups)); result.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, result); compiler.reportChangeToEnclosingScope(result); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy