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

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

There is a newer version: 9.0.8
Show newest version
/*
 * 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 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.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 transpiledFeatures = FeatureSet.ES6.without(FeatureSet.ES5); public EarlyEs6ToEs3Converter(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, this); TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, transpiledFeatures, this); } /** * 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 YIELD: if (n.isYieldAll()) { Es6ToEs3Util.preloadEs6RuntimeFunction(compiler, "makeIterator"); } Es6ToEs3Util.preloadEs6Symbol(compiler); 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"); } 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; case FUNCTION: if (n.isGeneratorFunction()) { Es6RewriteGenerators.preloadGeneratorSkeletonAndReportChange(compiler); } 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()); } 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) { checkArgument(node.isCall() || node.isArrayLit() || node.isNew()); List groups = new ArrayList<>(); Node currGroup = null; Node callee = node.isArrayLit() ? null : node.removeFirstChild(); Node currElement = node.removeFirstChild(); while (currElement != null) { if (currElement.isSpread()) { if (currGroup != null) { groups.add(currGroup); currGroup = null; } groups.add(Es6ToEs3Util.arrayFromIterable(compiler, currElement.removeFirstChild())); } else { if (currGroup == null) { currGroup = IR.arraylit(); } currGroup.addChildToBack(currElement); } currElement = node.removeFirstChild(); } if (currGroup != null) { groups.add(currGroup); } Node result = null; Node firstGroup = node.isNew() ? IR.arraylit(IR.nullNode()) : IR.arraylit(); Node joinedGroups = IR.call(IR.getprop(firstGroup, IR.string("concat")), groups.toArray(new Node[0])); if (node.isArrayLit()) { result = joinedGroups; } else if (node.isCall()) { if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) { 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 { Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode(); result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups); } } else { 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"); result = IR.newNode(IR.call(bindApply, callee, joinedGroups)); } result.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, result); compiler.reportChangeToEnclosingScope(result); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy