/** Tests {@link BasicErrorManager}. */ public class BasicErrorManagerTest extends TestCase { private static final String NULL_SOURCE = null; private LeveledJSErrorComparator comparator = new LeveledJSErrorComparator(); static final CheckLevel E = CheckLevel.ERROR; private static final DiagnosticType FOO_TYPE = DiagnosticType.error("TEST_FOO", "Foo"); private static final DiagnosticType JOO_TYPE = DiagnosticType.error("TEST_JOO", "Joo"); public void testOrderingBothNull() throws Exception { assertEquals(0, comparator.compare(null, null)); } public void testOrderingSourceName1() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE); JSError e2 = JSError.make("a", -1, -1, FOO_TYPE); assertSmaller(error(e1), error(e2)); } public void testOrderingSourceName2() throws Exception { JSError e1 = JSError.make("a", -1, -1, FOO_TYPE); JSError e2 = JSError.make("b", -1, -1, FOO_TYPE); assertSmaller(error(e1), error(e2)); } public void testOrderingLineno1() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE); JSError e2 = JSError.make(NULL_SOURCE, 2, -1, FOO_TYPE); assertSmaller(error(e1), error(e2)); } public void testOrderingLineno2() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, 8, -1, FOO_TYPE); JSError e2 = JSError.make(NULL_SOURCE, 56, -1, FOO_TYPE); assertSmaller(error(e1), error(e2)); } public void testOrderingCheckLevel() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE); JSError e2 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE); assertSmaller(warning(e1), error(e2)); } public void testOrderingCharno1() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, 5, -1, FOO_TYPE); JSError e2 = JSError.make(NULL_SOURCE, 5, 2, FOO_TYPE); assertSmaller(error(e1), error(e2)); // CheckLevel preempts charno comparison assertSmaller(warning(e1), error(e2)); } public void testOrderingCharno2() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, 8, 7, FOO_TYPE); JSError e2 = JSError.make(NULL_SOURCE, 8, 5, FOO_TYPE); assertSmaller(error(e2), error(e1)); // CheckLevel preempts charno comparison assertSmaller(warning(e2), error(e1)); } public void testOrderingDescription() throws Exception { JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE); JSError e2 = JSError.make(NULL_SOURCE, -1, -1, JOO_TYPE); assertSmaller(error(e1), error(e2)); } private ErrorWithLevel error(JSError e) { return new ErrorWithLevel(e, CheckLevel.ERROR); } private ErrorWithLevel warning(JSError e) { return new ErrorWithLevel(e, CheckLevel.WARNING); } private void assertSmaller(ErrorWithLevel p1, ErrorWithLevel p2) { int p1p2 = comparator.compare(p1, p2); assertTrue(Integer.toString(p1p2), p1p2 < 0); int p2p1 = comparator.compare(p2, p1); assertTrue(Integer.toString(p2p1), p2p1 > 0); } }
/** * Converts ES6 code to valid ES3 code. * * @author [email protected] (Tyler Breisacher) */ public class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapCompilerPass { private final AbstractCompiler compiler; static final DiagnosticType CANNOT_CONVERT = DiagnosticType.error("JSC_CANNOT_CONVERT", "This code cannot be converted from ES6 to ES3."); // TODO(tbreisacher): Remove this once all ES6 features are transpilable. static final DiagnosticType CANNOT_CONVERT_YET = DiagnosticType.error( "JSC_CANNOT_CONVERT_YET", "ES6-to-ES3 conversion of ''{0}'' is not yet implemented."); static final DiagnosticType DYNAMIC_EXTENDS_TYPE = DiagnosticType.error( "JSC_DYNAMIC_EXTENDS_TYPE", "The class in an extends clause must be a qualified name."); static final DiagnosticType STATIC_METHOD_REFERENCES_THIS = DiagnosticType.warning( "JSC_STATIC_METHOD_REFERENCES_THIS", "This static method uses the 'this' keyword." + " The transpiled output may be incorrect."); // The name of the var that captures 'this' for converting arrow functions. private static final String THIS_VAR = "$jscomp$this"; private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args"; private int freshSpreadVarCounter = 0; private static final String FRESH_COMP_PROP_VAR = "$jscomp$compprop"; private int freshPropVarCounter = 0; public Es6ToEs3Converter(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverse(compiler, scriptRoot, this); } /** * Arrow functions 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.getType()) { case Token.FUNCTION: if (n.isArrowFunction()) { visitArrowFunction(t, n); } break; case Token.ARRAY_COMP: case Token.ARRAY_PATTERN: case Token.FOR_OF: case Token.OBJECT_PATTERN: case Token.SUPER: cannotConvertYet(n, Token.name(n.getType())); // Don't bother visiting the children of a node if we // already know we can't convert the node itself. return false; } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.OBJECTLIT: for (Node child : n.children()) { if (child.isComputedProp()) { visitObjectWithComputedProperty(n, parent); break; } } break; case Token.STRING_KEY: visitStringKey(n); break; case Token.CLASS: visitClass(n, parent); break; case Token.PARAM_LIST: visitParamList(n, parent); break; case Token.ARRAYLIT: case Token.NEW: case Token.CALL: for (Node child : n.children()) { if (child.isSpread()) { visitArrayLitOrCallWithSpread(n, parent); break; } } break; } } /** Converts extended object literal {a} to {a:a}. */ private void visitStringKey(Node n) { if (!n.hasChildren()) { Node name = IR.name(n.getString()); name.copyInformationFrom(n); n.addChildToBack(name); compiler.reportCodeChange(); } } /** Processes trailing default and rest parameters. */ private void visitParamList(Node paramList, Node function) { Node insertSpot = null; Node block = function.getLastChild(); for (int i = 0; i < paramList.getChildCount(); i++) { Node param = paramList.getChildAtIndex(i); if (param.hasChildren()) { // default parameter param.setOptionalArg(true); Node defaultValue = param.removeFirstChild(); // Transpile to: param === undefined && (param = defaultValue); Node name = IR.name(param.getString()); Node undefined = IR.name("undefined"); Node stm = IR.exprResult( IR.and(IR.sheq(name, undefined), IR.assign(name.cloneNode(), defaultValue))); block.addChildAfter(stm.useSourceInfoIfMissingFromForTree(param), insertSpot); insertSpot = stm; compiler.reportCodeChange(); } else if (param.isRest()) { // rest parameter param.setType(Token.NAME); param.setVarArgs(true); // Transpile to: param = [].slice.call(arguments, i); Node newArr = IR.exprResult( IR.assign( IR.name(param.getString()), IR.call( IR.getprop( IR.getprop(IR.arraylit(), IR.string("slice")), IR.string("call")), IR.name("arguments"), IR.number(i)))); block.addChildAfter(newArr.useSourceInfoIfMissingFromForTree(param), insertSpot); compiler.reportCodeChange(); } } // 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. } /** * Processes array literals or calls containing spreads. Eg.: [1, 2, ...x, 4, 5] => [1, * 2].concat(x, [4, 5]); Eg.: f(...arr) => f.apply(null, arr) Eg.: new F(...args) => new * Function.prototype.bind.apply(F, [].concat(args)) */ private void visitArrayLitOrCallWithSpread(Node node, Node parent) { Preconditions.checkArgument(node.isCall() || node.isArrayLit() || node.isNew()); List<Node> 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(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 joinedGroups = IR.call( IR.getprop(IR.arraylit(), IR.string("concat")), groups.toArray(new Node[groups.size()])); 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 + freshSpreadVarCounter++); 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 { Node bindApply = NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), "Function.prototype.bind.apply"); result = IR.newNode(bindApply, callee, joinedGroups); } result.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, result); compiler.reportCodeChange(); } private void visitObjectWithComputedProperty(Node obj, Node parent) { Preconditions.checkArgument(obj.isObjectLit()); List<Node> props = new ArrayList<>(); Node currElement = obj.getFirstChild(); while (currElement != null) { if (currElement.isGetterDef() || currElement.isSetterDef()) { currElement = currElement.getNext(); } else { Node nextNode = currElement.getNext(); obj.removeChild(currElement); props.add(currElement); currElement = nextNode; } } String objName = FRESH_COMP_PROP_VAR + freshPropVarCounter++; props = Lists.reverse(props); Node result = IR.name(objName); for (Node propdef : props) { if (propdef.isComputedProp()) { Node propertyExpression = propdef.removeFirstChild(); Node value = propdef.removeFirstChild(); result = IR.comma(IR.assign(IR.getelem(IR.name(objName), propertyExpression), value), result); } else { if (!propdef.hasChildren()) { Node name = IR.name(propdef.getString()).copyInformationFrom(propdef); propdef.addChildToBack(name); } Node val = propdef.removeFirstChild(); propdef.setType(Token.STRING); int type = propdef.isQuotedString() ? Token.GETELEM : Token.GETPROP; Node access = new Node(type, IR.name(objName), propdef); result = IR.comma(IR.assign(access, val), result); } } Node statement = obj; while (!NodeUtil.isStatement(statement)) { statement = statement.getParent(); } result.useSourceInfoIfMissingFromForTree(obj); parent.replaceChild(obj, result); Node var = IR.var(IR.name(objName), obj); var.useSourceInfoIfMissingFromForTree(statement); statement.getParent().addChildBefore(var, statement); compiler.reportCodeChange(); } private void visitClass(Node classNode, Node parent) { Node className = classNode.getFirstChild(); Node superClassName = className.getNext(); Node classMembers = classNode.getLastChild(); // This is a statement node. We insert methods of the // transpiled class after this node. Node insertionPoint; // The fully qualified name of the class, which will be used in the output. // May come from the class itself or the LHS of an assignment. String fullClassName = null; // Whether the constructor function in the output should be anonymous. boolean anonymous; // If this is a class statement, or a class expression in a simple // assignment or var statement, convert it. In any other case, the // code is too dynamic, so just call cannotConvert. if (NodeUtil.isStatement(classNode)) { fullClassName = className.getString(); anonymous = false; insertionPoint = classNode; } else if (parent.isAssign() && parent.getParent().isExprResult()) { // Add members after the EXPR_RESULT node: // example.C = class {}; example.C.prototype.foo = function() {}; fullClassName = parent.getFirstChild().getQualifiedName(); if (fullClassName == null) { cannotConvert(parent); return; } anonymous = true; insertionPoint = parent.getParent(); } else if (parent.isName()) { // Add members after the 'var' statement. // var C = class {}; C.prototype.foo = function() {}; fullClassName = parent.getString(); anonymous = true; insertionPoint = parent.getParent(); } else { cannotConvert(parent); return; } Verify.verify(NodeUtil.isStatement(insertionPoint)); className.detachFromParent(); Node constructor = null; JSDocInfo ctorJSDocInfo = null; for (Node member : classMembers.children()) { if (member.getString().equals("constructor")) { ctorJSDocInfo = member.getJSDocInfo(); constructor = member.getFirstChild().detachFromParent(); if (!anonymous) { constructor.replaceChild(constructor.getFirstChild(), className); } } else { String qualifiedMemberName; if (member.isStaticMember()) { if (NodeUtil.referencesThis(member.getFirstChild())) { compiler.report(JSError.make(member, STATIC_METHOD_REFERENCES_THIS)); } qualifiedMemberName = Joiner.on(".").join(fullClassName, member.getString()); } else { qualifiedMemberName = Joiner.on(".").join(fullClassName, "prototype", member.getString()); } Node assign = IR.assign( NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), qualifiedMemberName, /* basis node */ member, /* original name */ member.getString()), member.getFirstChild().detachFromParent()); assign.srcref(member); JSDocInfo info = member.getJSDocInfo(); if (info != null) { info.setAssociatedNode(assign); assign.setJSDocInfo(info); } Node newNode = NodeUtil.newExpr(assign); insertionPoint.getParent().addChildAfter(newNode, insertionPoint); insertionPoint = newNode; } } if (constructor == null) { Node name = anonymous ? IR.name("").srcref(className) : className; constructor = IR.function(name, IR.paramList().srcref(classNode), IR.block().srcref(classNode)); } JSDocInfo classJSDoc = classNode.getJSDocInfo(); JSDocInfoBuilder newInfo = (classJSDoc != null) ? JSDocInfoBuilder.copyFrom(classJSDoc) : new JSDocInfoBuilder(true); newInfo.recordConstructor(); if (!superClassName.isEmpty()) { if (!superClassName.isQualifiedName()) { compiler.report(JSError.make(superClassName, DYNAMIC_EXTENDS_TYPE)); return; } Node superClassString = IR.string(superClassName.getQualifiedName()); if (newInfo.isInterfaceRecorded()) { newInfo.recordExtendedInterface( new JSTypeExpression( new Node(Token.BANG, superClassString), superClassName.getSourceFileName())); } else { // TODO(mattloring) Remove dependency on Closure Library. Node inherits = NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), "goog.inherits"); Node inheritsCall = IR.exprResult(IR.call(inherits, className.cloneTree(), superClassName.cloneTree())); inheritsCall.useSourceInfoIfMissingFromForTree(classNode); parent.addChildAfter(inheritsCall, classNode); newInfo.recordBaseType( new JSTypeExpression( new Node(Token.BANG, superClassString), superClassName.getSourceFileName())); } } // Classes are @struct by default. if (!newInfo.isUnrestrictedRecorded() && !newInfo.isDictRecorded() && !newInfo.isStructRecorded()) { newInfo.recordStruct(); } if (ctorJSDocInfo != null) { newInfo.recordSuppressions(ctorJSDocInfo.getSuppressions()); for (String param : ctorJSDocInfo.getParameterNames()) { newInfo.recordParameter(param, ctorJSDocInfo.getParameterType(param)); } } parent.replaceChild(classNode, constructor); if (NodeUtil.isStatement(constructor)) { constructor.setJSDocInfo(newInfo.build(constructor)); } else if (parent.isName()) { // The constructor function is the RHS of a var statement. // Add the JSDoc to the VAR node. Node var = parent.getParent(); var.setJSDocInfo(newInfo.build(var)); } else if (parent.isAssign()) { // The constructor function is the RHS of an assignment. // Add the JSDoc to the ASSIGN node. parent.setJSDocInfo(newInfo.build(parent)); } else { throw new IllegalStateException("Unexpected parent node " + parent); } compiler.reportCodeChange(); } /** Converts ES6 arrow functions to standard anonymous ES3 functions. */ private void visitArrowFunction(NodeTraversal t, Node n) { n.setIsArrowFunction(false); Node body = n.getLastChild(); if (!body.isBlock()) { body.detachFromParent(); Node newBody = IR.block(IR.returnNode(body).srcref(body)).srcref(body); n.addChildToBack(newBody); } UpdateThisNodes thisUpdater = new UpdateThisNodes(); NodeTraversal.traverse(compiler, body, thisUpdater); if (thisUpdater.changed) { addThisVar(t); } compiler.reportCodeChange(); } private void addThisVar(NodeTraversal t) { Scope scope = t.getScope(); if (scope.isDeclared(THIS_VAR, false)) { return; } Node parent = t.getScopeRoot(); if (parent.isFunction()) { // Add the new node at the beginning of the function body. parent = parent.getLastChild(); } if (parent.isSyntheticBlock()) { // Add the new node inside the SCRIPT node instead of the // synthetic block that contains it. parent = parent.getFirstChild(); } Node name = IR.name(THIS_VAR).srcref(parent); Node thisVar = IR.var(name, IR.thisNode().srcref(parent)); thisVar.srcref(parent); parent.addChildToFront(thisVar); scope.declare(THIS_VAR, name, null, compiler.getInput(parent.getInputId())); } private static class UpdateThisNodes implements NodeTraversal.Callback { private boolean changed = false; @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isThis()) { Node name = IR.name(THIS_VAR).srcref(n); parent.replaceChild(n, name); changed = true; } } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return !n.isFunction() || n.isArrowFunction(); } } private void cannotConvert(Node n) { compiler.report(JSError.make(n, CANNOT_CONVERT)); } /** * Warns the user that the given ES6 feature cannot be converted to ES3 because the transpilation * is not yet implemented. A call to this method is essentially a "TODO(tbreisacher): Implement * {@code feature}" comment. */ private void cannotConvertYet(Node n, String feature) { compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature)); } }
/** * Creates synthetic blocks to optimizations from moving code past markers in the source. * * @author [email protected] (John Lenz) */ class CreateSyntheticBlocks implements CompilerPass { static final DiagnosticType UNMATCHED_START_MARKER = DiagnosticType.error("JSC_UNMATCHED_START_MARKER", "Unmatched {0}"); static final DiagnosticType UNMATCHED_END_MARKER = DiagnosticType.error("JSC_UNMATCHED_END_MARKER", "Unmatched {1} - {0} not in the same block"); static final DiagnosticType INVALID_MARKER_USAGE = DiagnosticType.error( "JSC_INVALID_MARKER_USAGE", "Marker {0} can only be used in a simple " + "call expression"); private final AbstractCompiler compiler; /** Name of the start marker. */ private final String startMarkerName; /** Name of the end marker. */ private final String endMarkerName; /** Markers can be nested. */ private final Deque<Node> markerStack = new ArrayDeque<>(); private final List<Marker> validMarkers = new ArrayList<>(); private static class Marker { final Node startMarker; final Node endMarker; public Marker(Node startMarker, Node endMarker) { this.startMarker = startMarker; this.endMarker = endMarker; } } public CreateSyntheticBlocks( AbstractCompiler compiler, String startMarkerName, String endMarkerName) { this.compiler = compiler; this.startMarkerName = startMarkerName; this.endMarkerName = endMarkerName; } @Override public void process(Node externs, Node root) { // Find and validate the markers. NodeTraversal.traverseEs6(compiler, root, new Callback()); // Complain about any unmatched markers. for (Node node : markerStack) { compiler.report(JSError.make(node, UNMATCHED_START_MARKER, startMarkerName)); } // Add the block for the valid marker sets. for (Marker marker : validMarkers) { addBlocks(marker); } } /** @param marker The marker to add synthetic blocks for. */ private void addBlocks(Marker marker) { // Add block around the template section so that it looks like this: // BLOCK (synthetic) // START // BLOCK (synthetic) // BODY // END // This prevents the start or end markers from mingling with the code // in the block body. Node originalParent = marker.endMarker.getParent(); Node outerBlock = IR.block(); outerBlock.setIsSyntheticBlock(true); originalParent.addChildBefore(outerBlock, marker.startMarker); Node innerBlock = IR.block(); innerBlock.setIsSyntheticBlock(true); // Move everything after the start Node up to the end Node into the inner // block. moveSiblingExclusive(originalParent, innerBlock, marker.startMarker, marker.endMarker); // Add the start node. outerBlock.addChildToBack(originalParent.removeChildAfter(outerBlock)); // Add the inner block outerBlock.addChildToBack(innerBlock); // and finally the end node. outerBlock.addChildToBack(originalParent.removeChildAfter(outerBlock)); compiler.reportCodeChange(); } /** * Move the Nodes between start and end from the source block to the destination block. If start * is null, move the first child of the block. If end is null, move the last child of the block. */ private void moveSiblingExclusive(Node src, Node dest, @Nullable Node start, @Nullable Node end) { while (childAfter(src, start) != end) { Node child = src.removeFirstOrChildAfter(start); dest.addChildToBack(child); } } /** Like Node.getNext, that null is used to signal the child before the block. */ private static Node childAfter(Node parent, @Nullable Node siblingBefore) { if (siblingBefore == null) { return parent.getFirstChild(); } else { return siblingBefore.getNext(); } } private class Callback extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.isCall() || !n.getFirstChild().isName()) { return; } Node callTarget = n.getFirstChild(); String callName = callTarget.getString(); if (startMarkerName.equals(callName)) { if (!parent.isExprResult()) { compiler.report(t.makeError(n, INVALID_MARKER_USAGE, startMarkerName)); return; } markerStack.push(parent); return; } if (!endMarkerName.equals(callName)) { return; } Node endMarkerNode = parent; if (!endMarkerNode.isExprResult()) { compiler.report(t.makeError(n, INVALID_MARKER_USAGE, endMarkerName)); return; } if (markerStack.isEmpty()) { compiler.report(t.makeError(n, UNMATCHED_END_MARKER, startMarkerName, endMarkerName)); return; } Node startMarkerNode = markerStack.pop(); if (endMarkerNode.getParent() != startMarkerNode.getParent()) { // The end marker isn't in the same block as the start marker. compiler.report(t.makeError(n, UNMATCHED_END_MARKER, startMarkerName, endMarkerName)); return; } // This is a valid marker set add it to the list of markers to process. validMarkers.add(new Marker(startMarkerNode, endMarkerNode)); } } }
/** * A pass for stripping a list of provided Javascript object types. * * <p>The stripping strategy is as follows: - Provide: 1) a list of types that should be stripped, * and 2) a list of suffixes of field/variable names that should be stripped. - Remove declarations * of variables that are initialized using static methods of strip types (e.g. var x = * goog.debug.Logger.getLogger(...);). - Remove all references to variables that are stripped. - * Remove all object literal keys with strip names. - Remove all assignments to 1) field names that * are strip names and 2) qualified names that begin with strip types. - Remove all statements * containing calls to static methods of strip types. */ class StripCode implements CompilerPass { // TODO(user): Try eliminating the need for a list of strip names by instead // recording which field names are assigned to debug types in each js input. private final AbstractCompiler compiler; private final Set<String> stripTypes; private final Set<String> stripNameSuffixes; private final Set<String> stripTypePrefixes; private final Set<String> stripNamePrefixes; private final Set<Scope.Var> varsToRemove; static final DiagnosticType STRIP_TYPE_INHERIT_ERROR = DiagnosticType.error( "JSC_STRIP_TYPE_INHERIT_ERROR", "Non-strip type {0} cannot inherit from strip type {1}"); static final DiagnosticType STRIP_ASSIGNMENT_ERROR = DiagnosticType.error("JSC_STRIP_ASSIGNMENT_ERROR", "Unable to strip assignment to {0}"); /** * Creates an instance. * * @param compiler The compiler */ StripCode( AbstractCompiler compiler, Set<String> stripTypes, Set<String> stripNameSuffixes, Set<String> stripTypePrefixes, Set<String> stripNamePrefixes) { this.compiler = compiler; this.stripTypes = Sets.newHashSet(stripTypes); this.stripNameSuffixes = Sets.newHashSet(stripNameSuffixes); this.stripTypePrefixes = Sets.newHashSet(stripTypePrefixes); this.stripNamePrefixes = Sets.newHashSet(stripNamePrefixes); this.varsToRemove = Sets.newHashSet(); } /** Enables stripping of goog.tweak functions. */ public void enableTweakStripping() { stripTypes.add("goog.tweak"); } public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, new Strip()); } // ------------------------------------------------------------------------- /** A callback that strips debug code from a Javascript parse tree. */ private class Strip extends AbstractPostOrderCallback { public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.VAR: removeVarDeclarationsByNameOrRvalue(t, n, parent); break; case Token.NAME: maybeRemoveReferenceToRemovedVariable(t, n, parent); break; case Token.ASSIGN: case Token.ASSIGN_BITOR: case Token.ASSIGN_BITXOR: case Token.ASSIGN_BITAND: case Token.ASSIGN_LSH: case Token.ASSIGN_RSH: case Token.ASSIGN_URSH: case Token.ASSIGN_ADD: case Token.ASSIGN_SUB: case Token.ASSIGN_MUL: case Token.ASSIGN_DIV: case Token.ASSIGN_MOD: maybeEliminateAssignmentByLvalueName(t, n, parent); break; case Token.CALL: case Token.NEW: maybeRemoveCall(t, n, parent); break; case Token.OBJECTLIT: eliminateKeysWithStripNamesFromObjLit(t, n); break; case Token.EXPR_RESULT: maybeEliminateExpressionByName(t, n, parent); break; } } /** * Removes declarations of any variables whose names are strip names or whose whose rvalues are * static method calls on strip types. Builds a set of removed variables so that all references * to them can be removed. * * @param t The traversal * @param n A VAR node * @param parent {@code n}'s parent */ void removeVarDeclarationsByNameOrRvalue(NodeTraversal t, Node n, Node parent) { for (Node nameNode = n.getFirstChild(); nameNode != null; nameNode = nameNode.getNext()) { String name = nameNode.getString(); if (isStripName(name) || isCallWhoseReturnValueShouldBeStripped(nameNode.getFirstChild())) { // Remove the NAME. Scope scope = t.getScope(); varsToRemove.add(scope.getVar(name)); n.removeChild(nameNode); compiler.reportCodeChange(); } } if (!n.hasChildren()) { // Must also remove the VAR. replaceWithEmpty(n, parent); compiler.reportCodeChange(); } } /** * Removes a reference if it is a reference to a removed variable. * * @param t The traversal * @param n A NAME node * @param parent {@code n}'s parent */ void maybeRemoveReferenceToRemovedVariable(NodeTraversal t, Node n, Node parent) { switch (parent.getType()) { case Token.VAR: // This is a variable decalaration, not a reference. break; case Token.GETPROP: // GETPROP // NAME // STRING (property name) case Token.GETELEM: // GETELEM // NAME // NUMBER|STRING|NAME|... if (parent.getFirstChild() == n && isReferenceToRemovedVar(t, n)) { replaceHighestNestedCallWithNull(parent, parent.getParent()); } break; case Token.ASSIGN: case Token.ASSIGN_BITOR: case Token.ASSIGN_BITXOR: case Token.ASSIGN_BITAND: case Token.ASSIGN_LSH: case Token.ASSIGN_RSH: case Token.ASSIGN_URSH: case Token.ASSIGN_ADD: case Token.ASSIGN_SUB: case Token.ASSIGN_MUL: case Token.ASSIGN_DIV: case Token.ASSIGN_MOD: if (isReferenceToRemovedVar(t, n)) { if (parent.getFirstChild() == n) { Node gramps = parent.getParent(); if (NodeUtil.isExpressionNode(gramps)) { // Remove the assignment. Node greatGramps = gramps.getParent(); replaceWithEmpty(gramps, greatGramps); compiler.reportCodeChange(); } else { // Substitute the rvalue for the assignment. Node rvalue = n.getNext(); parent.removeChild(rvalue); gramps.replaceChild(parent, rvalue); compiler.reportCodeChange(); } } else { // The var reference is the rvalue. Replace it with null. replaceWithNull(n, parent); compiler.reportCodeChange(); } } break; default: if (isReferenceToRemovedVar(t, n)) { replaceWithNull(n, parent); compiler.reportCodeChange(); } break; } } /** * Use a while loop to get up out of any nested calls. For example, if we have just detected * that we need to remove the a.b() call in a.b().c().d(), we'll have to remove all of the * calls, and it will take a few iterations through this loop to get up to d(). */ void replaceHighestNestedCallWithNull(Node node, Node parent) { Node ancestor = parent; Node ancestorChild = node; while (true) { if (ancestor.getFirstChild() != ancestorChild) { replaceWithNull(ancestorChild, ancestor); break; } if (NodeUtil.isExpressionNode(ancestor)) { // Remove the entire expression statement. Node ancParent = ancestor.getParent(); replaceWithEmpty(ancestor, ancParent); break; } int type = ancestor.getType(); if (type != Token.GETPROP && type != Token.GETELEM && type != Token.CALL) { replaceWithNull(ancestorChild, ancestor); break; } ancestorChild = ancestor; ancestor = ancestor.getParent(); } compiler.reportCodeChange(); } /** * Eliminates an assignment if the lvalue is: - A field name that's a strip name - A qualified * name that begins with a strip type * * @param t The traversal * @param n An ASSIGN node * @param parent {@code n}'s parent */ void maybeEliminateAssignmentByLvalueName(NodeTraversal t, Node n, Node parent) { // ASSIGN // lvalue // rvalue Node lvalue = n.getFirstChild(); if (nameEndsWithFieldNameToStrip(lvalue) || qualifiedNameBeginsWithStripType(lvalue)) { // Limit to EXPR_RESULT because it is not // safe to eliminate assignment in complex expressions, // e.g. in ((x = 7) + 8) if (NodeUtil.isExpressionNode(parent)) { Node gramps = parent.getParent(); replaceWithEmpty(parent, gramps); compiler.reportCodeChange(); } else { t.report(n, STRIP_ASSIGNMENT_ERROR, lvalue.getQualifiedName()); } } } /** * Eliminates an expression if it refers to: - A field name that's a strip name - A qualified * name that begins with a strip type This gets rid of construct like: a.prototype.logger; (used * instead of a.prototype.logger = null;) This expression is not an assignment and so will not * be caught by maybeEliminateAssignmentByLvalueName. * * @param t The traversal * @param n An EXPR_RESULT node * @param parent {@code n}'s parent */ void maybeEliminateExpressionByName(NodeTraversal t, Node n, Node parent) { // EXPR_RESULT // expression Node expression = n.getFirstChild(); if (nameEndsWithFieldNameToStrip(expression) || qualifiedNameBeginsWithStripType(expression)) { if (NodeUtil.isExpressionNode(parent)) { Node gramps = parent.getParent(); replaceWithEmpty(parent, gramps); } else { replaceWithEmpty(n, parent); } compiler.reportCodeChange(); } } /** * Removes a method call if {@link #isMethodOrCtorCallThatTriggersRemoval} indicates that it * should be removed. * * @param t The traversal * @param n A CALL node * @param parent {@code n}'s parent */ void maybeRemoveCall(NodeTraversal t, Node n, Node parent) { // CALL/NEW // function // arguments if (isMethodOrCtorCallThatTriggersRemoval(t, n, parent)) { replaceHighestNestedCallWithNull(n, parent); } } /** * Eliminates any object literal keys in an object literal declaration that have strip names. * * @param t The traversal * @param n An OBJLIT node */ void eliminateKeysWithStripNamesFromObjLit(NodeTraversal t, Node n) { // OBJLIT // key1 // value1 // key2 // ... Node key = n.getFirstChild(); while (key != null) { if (isStripName(key.getString())) { Node value = key.getFirstChild(); Node next = key.getNext(); n.removeChild(key); key = next; compiler.reportCodeChange(); } else { key = key.getNext(); } } } /** * Gets whether a node is a CALL node whose return value should be stripped. A call's return * value should be stripped if the function getting called is a static method in a class that * gets stripped. For example, if "goog.debug.Logger" is a strip name, then this function * returns true for a call such as "goog.debug.Logger.getLogger(...)". It may also simply be a * function that is getting stripped. For example, if "getLogger" is a strip name, but not * "goog.debug.Logger", this will still return true. * * @param n A node (typically a CALL node) * @return Whether the call's return value should be stripped */ boolean isCallWhoseReturnValueShouldBeStripped(@Nullable Node n) { return n != null && (n.getType() == Token.CALL || n.getType() == Token.NEW) && n.hasChildren() && (qualifiedNameBeginsWithStripType(n.getFirstChild()) || nameEndsWithFieldNameToStrip(n.getFirstChild())); } /** * Gets whether a qualified name begins with a strip name. The names "goog.debug", * "goog.debug.Logger", and "goog.debug.Logger.Level" are examples of strip names that would * result in this function returning true for a node representing the name * "goog.debug.Logger.Level". * * @param n A node (typically a NAME or GETPROP node) * @return Whether the name begins with a strip name */ boolean qualifiedNameBeginsWithStripType(Node n) { String name = n.getQualifiedName(); return qualifiedNameBeginsWithStripType(name); } /** * Gets whether a qualified name begins with a strip name. The names "goog.debug", * "goog.debug.Logger", and "goog.debug.Logger.Level" are examples of strip names that would * result in this function returning true for a node representing the name * "goog.debug.Logger.Level". * * @param name A qualified class name * @return Whether the name begins with a strip name */ boolean qualifiedNameBeginsWithStripType(String name) { if (name != null) { for (String type : stripTypes) { if (name.equals(type) || name.startsWith(type + ".")) { return true; } } for (String type : stripTypePrefixes) { if (name.startsWith(type)) { return true; } } } return false; } /** * Determines whether a NAME node represents a reference to a variable that has been removed. * * @param t The traversal * @param n A NAME node * @return Whether the variable was removed */ boolean isReferenceToRemovedVar(NodeTraversal t, Node n) { String name = n.getString(); Scope scope = t.getScope(); Scope.Var var = scope.getVar(name); return varsToRemove.contains(var); } /** * Gets whether a CALL node triggers statement removal, based on the name of the object whose * method is being called, or the name of the method. Checks whether the name begins with a * strip type, ends with a field name that's a strip name, or belongs to the set of global * class-defining functions (e.g. goog.inherits). * * @param t The traversal * @param n A CALL node * @return Whether the node triggers statement removal */ boolean isMethodOrCtorCallThatTriggersRemoval(NodeTraversal t, Node n, Node parent) { // CALL/NEW // GETPROP (function) <-- we're interested in this, the function // GETPROP (callee object) <-- or the object on which it is called // ... // STRING (field name) // STRING (method name) // ... (arguments) Node function = n.getFirstChild(); if (function == null || function.getType() != Token.GETPROP) { // We are only interested in calls on object references that are // properties. We don't need to eliminate method calls on variables // that are getting removed, since that's already done by the code // that removes all references to those variables. return false; } if (parent != null && parent.getType() == Token.NAME) { Node gramps = parent.getParent(); if (gramps != null && gramps.getType() == Token.VAR) { // The call's return value is being used to initialize a newly // declared variable. We should leave the call intact for now. // That way, when the traversal reaches the variable declaration, // we'll recognize that the variable and all references to it need // to be eliminated. return false; } } Node callee = function.getFirstChild(); return nameEndsWithFieldNameToStrip(callee) || nameEndsWithFieldNameToStrip(function) || qualifiedNameBeginsWithStripType(function) || actsOnStripType(t, n); } /** * Gets whether a name ends with a field name that should be stripped. For example, this * function would return true when passed "this.logger" or "a.b.c.myLogger" if "logger" is a * strip name. * * @param n A node (typically a GETPROP node) * @return Whether the name ends with a field name that should be stripped */ boolean nameEndsWithFieldNameToStrip(@Nullable Node n) { if (n != null && n.getType() == Token.GETPROP) { Node propNode = n.getLastChild(); return propNode != null && propNode.getType() == Token.STRING && isStripName(propNode.getString()); } return false; } /** * Determines whether the given node helps to define a strip type. For example, * goog.inherits(stripType, Object) would be such a call. * * <p>Also reports an error if a non-strip type inherits from a strip type. * * @param t The current traversal * @param callNode The CALL node */ private boolean actsOnStripType(NodeTraversal t, Node callNode) { SubclassRelationship classes = compiler.getCodingConvention().getClassesDefinedByCall(callNode); if (classes != null) { // It's okay to strip a type that inherits from a non-stripped type // e.g. goog.inherits(goog.debug.Logger, Object) if (qualifiedNameBeginsWithStripType(classes.subclassName)) { return true; } // report an error if a non-strip type inherits from a // strip type. if (qualifiedNameBeginsWithStripType(classes.superclassName)) { t.report( callNode, STRIP_TYPE_INHERIT_ERROR, classes.subclassName, classes.superclassName); } } return false; } /** * Gets whether a Javascript identifier is the name of a variable or property that should be * stripped. * * @param name A Javascript identifier * @return Whether {@code name} is a name that triggers removal */ boolean isStripName(String name) { if (stripNameSuffixes.contains(name) || stripNamePrefixes.contains(name)) { return true; } if ((name.length() == 0) || Character.isUpperCase(name.charAt(0))) { return false; } String lcName = name.toLowerCase(); for (String stripName : stripNamePrefixes) { if (lcName.startsWith(stripName.toLowerCase())) { return true; } } for (String stripName : stripNameSuffixes) { if (lcName.endsWith(stripName.toLowerCase())) { return true; } } return false; } /** * Replaces a node with a NULL node. This is useful where a value is expected. * * @param n A node * @param parent {@code n}'s parent */ void replaceWithNull(Node n, Node parent) { parent.replaceChild(n, new Node(Token.NULL)); } /** * Replaces a node with an EMPTY node. This is useful where a statement is expected. * * @param n A node * @param parent {@code n}'s parent */ void replaceWithEmpty(Node n, Node parent) { NodeUtil.removeChild(parent, n); } } }
/** * Process goog.tweak primitives. Checks that: * * <ul> * <li>parameters to goog.tweak.register* are literals of the correct type. * <li>the parameter to goog.tweak.get* is a string literal. * <li>parameters to goog.tweak.overrideDefaultValue are literals of the correct type. * <li>tweak IDs passed to goog.tweak.get* and goog.tweak.overrideDefaultValue correspond to * registered tweaks. * <li>all calls to goog.tweak.register* and goog.tweak.overrideDefaultValue are within the * top-level context. * <li>each tweak is registered only once. * <li>calls to goog.tweak.overrideDefaultValue occur before the call to the corresponding * goog.tweak.register* function. * </ul> * * @author [email protected] (Andrew Grieve) */ class ProcessTweaks implements CompilerPass { private final AbstractCompiler compiler; private final boolean stripTweaks; private final SortedMap<String, Node> compilerDefaultValueOverrides; private static final CharMatcher ID_MATCHER = CharMatcher.inRange('a', 'z') .or(CharMatcher.inRange('A', 'Z')) .or(CharMatcher.anyOf("0123456789_.")); // Warnings and Errors. static final DiagnosticType UNKNOWN_TWEAK_WARNING = DiagnosticType.warning("JSC_UNKNOWN_TWEAK_WARNING", "no tweak registered with ID {0}"); static final DiagnosticType TWEAK_MULTIPLY_REGISTERED_ERROR = DiagnosticType.error( "JSC_TWEAK_MULTIPLY_REGISTERED_ERROR", "Tweak {0} has already been registered."); static final DiagnosticType NON_LITERAL_TWEAK_ID_ERROR = DiagnosticType.error("JSC_NON_LITERAL_TWEAK_ID_ERROR", "tweak ID must be a string literal"); static final DiagnosticType INVALID_TWEAK_DEFAULT_VALUE_WARNING = DiagnosticType.warning( "JSC_INVALID_TWEAK_DEFAULT_VALUE_WARNING", "tweak {0} registered with {1} must have a default value that is a " + "literal of type {2}"); static final DiagnosticType NON_GLOBAL_TWEAK_INIT_ERROR = DiagnosticType.error( "JSC_NON_GLOBAL_TWEAK_INIT_ERROR", "tweak declaration {0} must occur in the global scope"); static final DiagnosticType TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR = DiagnosticType.error( "JSC_TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR", "Cannot override the default value of tweak {0} after it has been " + "registered"); static final DiagnosticType TWEAK_WRONG_GETTER_TYPE_WARNING = DiagnosticType.warning( "JSC_TWEAK_WRONG_GETTER_TYPE_WARNING", "tweak getter function {0} used for tweak registered using {1}"); static final DiagnosticType INVALID_TWEAK_ID_ERROR = DiagnosticType.error( "JSC_INVALID_TWEAK_ID_ERROR", "tweak ID contains illegal characters. Only letters, numbers, _ " + "and . are allowed"); /** An enum of goog.tweak functions. */ private static enum TweakFunction { REGISTER_BOOLEAN("goog.tweak.registerBoolean", "boolean", Token.TRUE, Token.FALSE), REGISTER_NUMBER("goog.tweak.registerNumber", "number", Token.NUMBER), REGISTER_STRING("goog.tweak.registerString", "string", Token.STRING), OVERRIDE_DEFAULT_VALUE("goog.tweak.overrideDefaultValue"), GET_COMPILER_OVERRIDES("goog.tweak.getCompilerOverrides_"), GET_BOOLEAN("goog.tweak.getBoolean", REGISTER_BOOLEAN), GET_NUMBER("goog.tweak.getNumber", REGISTER_NUMBER), GET_STRING("goog.tweak.getString", REGISTER_STRING); final String name; final String expectedTypeName; final int validNodeTypeA; final int validNodeTypeB; final TweakFunction registerFunction; TweakFunction(String name) { this(name, null, Token.ERROR, Token.ERROR, null); } TweakFunction(String name, String expectedTypeName, int validNodeTypeA) { this(name, expectedTypeName, validNodeTypeA, Token.ERROR, null); } TweakFunction(String name, String expectedTypeName, int validNodeTypeA, int validNodeTypeB) { this(name, expectedTypeName, validNodeTypeA, validNodeTypeB, null); } TweakFunction(String name, TweakFunction registerFunction) { this(name, null, Token.ERROR, Token.ERROR, registerFunction); } TweakFunction( String name, String expectedTypeName, int validNodeTypeA, int validNodeTypeB, TweakFunction registerFunction) { this.name = name; this.expectedTypeName = expectedTypeName; this.validNodeTypeA = validNodeTypeA; this.validNodeTypeB = validNodeTypeB; this.registerFunction = registerFunction; } boolean isValidNodeType(int type) { return type == validNodeTypeA || type == validNodeTypeB; } boolean isCorrectRegisterFunction(TweakFunction registerFunction) { Preconditions.checkNotNull(registerFunction); return this.registerFunction == registerFunction; } boolean isGetterFunction() { return registerFunction != null; } String getName() { return name; } String getExpectedTypeName() { return expectedTypeName; } Node createDefaultValueNode() { switch (this) { case REGISTER_BOOLEAN: return IR.falseNode(); case REGISTER_NUMBER: return IR.number(0); case REGISTER_STRING: return IR.string(""); default: throw new IllegalStateException(); } } } // A map of function name -> TweakFunction. private static final Map<String, TweakFunction> TWEAK_FUNCTIONS_MAP; static { TWEAK_FUNCTIONS_MAP = Maps.newHashMap(); for (TweakFunction func : TweakFunction.values()) { TWEAK_FUNCTIONS_MAP.put(func.getName(), func); } } ProcessTweaks( AbstractCompiler compiler, boolean stripTweaks, Map<String, Node> compilerDefaultValueOverrides) { this.compiler = compiler; this.stripTweaks = stripTweaks; // Having the map sorted is required for the unit tests to be deterministic. this.compilerDefaultValueOverrides = Maps.newTreeMap(); this.compilerDefaultValueOverrides.putAll(compilerDefaultValueOverrides); } @Override public void process(Node externs, Node root) { CollectTweaksResult result = collectTweaks(root); applyCompilerDefaultValueOverrides(result.tweakInfos); boolean changed = false; if (stripTweaks) { changed = stripAllCalls(result.tweakInfos); } else if (!compilerDefaultValueOverrides.isEmpty()) { changed = replaceGetCompilerOverridesCalls(result.getOverridesCalls); } if (changed) { compiler.reportCodeChange(); } } /** * Passes the compiler default value overrides to the JS by replacing calls to * goog.tweak.getCompilerOverrids_ with a map of tweak ID->default value; */ private boolean replaceGetCompilerOverridesCalls(List<TweakFunctionCall> calls) { for (TweakFunctionCall call : calls) { Node callNode = call.callNode; Node objNode = createCompilerDefaultValueOverridesVarNode(callNode); callNode.getParent().replaceChild(callNode, objNode); } return !calls.isEmpty(); } /** * Removes all CALL nodes in the given TweakInfos, replacing calls to getter functions with the * tweak's default value. */ private boolean stripAllCalls(Map<String, TweakInfo> tweakInfos) { for (TweakInfo tweakInfo : tweakInfos.values()) { boolean isRegistered = tweakInfo.isRegistered(); for (TweakFunctionCall functionCall : tweakInfo.functionCalls) { Node callNode = functionCall.callNode; Node parent = callNode.getParent(); if (functionCall.tweakFunc.isGetterFunction()) { Node newValue; if (isRegistered) { newValue = tweakInfo.getDefaultValueNode().cloneNode(); } else { // When we find a getter of an unregistered tweak, there has // already been a warning about it, so now just use a default // value when stripping. TweakFunction registerFunction = functionCall.tweakFunc.registerFunction; newValue = registerFunction.createDefaultValueNode(); } parent.replaceChild(callNode, newValue); } else { Node voidZeroNode = IR.voidNode(IR.number(0).srcref(callNode)).srcref(callNode); parent.replaceChild(callNode, voidZeroNode); } } } return !tweakInfos.isEmpty(); } /** Creates a JS object that holds a map of tweakId -> default value override. */ private Node createCompilerDefaultValueOverridesVarNode(Node sourceInformationNode) { Node objNode = IR.objectlit().srcref(sourceInformationNode); for (Entry<String, Node> entry : compilerDefaultValueOverrides.entrySet()) { Node objKeyNode = IR.stringKey(entry.getKey()).copyInformationFrom(sourceInformationNode); Node objValueNode = entry.getValue().cloneNode().copyInformationFrom(sourceInformationNode); objKeyNode.addChildToBack(objValueNode); objNode.addChildToBack(objKeyNode); } return objNode; } /** Sets the default values of tweaks based on compiler options. */ private void applyCompilerDefaultValueOverrides(Map<String, TweakInfo> tweakInfos) { for (Entry<String, Node> entry : compilerDefaultValueOverrides.entrySet()) { String tweakId = entry.getKey(); TweakInfo tweakInfo = tweakInfos.get(tweakId); if (tweakInfo == null) { compiler.report(JSError.make(UNKNOWN_TWEAK_WARNING, tweakId)); } else { TweakFunction registerFunc = tweakInfo.registerCall.tweakFunc; Node value = entry.getValue(); if (!registerFunc.isValidNodeType(value.getType())) { compiler.report( JSError.make( INVALID_TWEAK_DEFAULT_VALUE_WARNING, tweakId, registerFunc.getName(), registerFunc.getExpectedTypeName())); } else { tweakInfo.defaultValueNode = value; } } } } /** * Finds all calls to goog.tweak functions and emits warnings/errors if any of the calls have * issues. * * @return A map of {@link TweakInfo} structures, keyed by tweak ID. */ private CollectTweaksResult collectTweaks(Node root) { CollectTweaks pass = new CollectTweaks(); NodeTraversal.traverse(compiler, root, pass); Map<String, TweakInfo> tweakInfos = pass.allTweaks; for (TweakInfo tweakInfo : tweakInfos.values()) { tweakInfo.emitAllWarnings(); } return new CollectTweaksResult(tweakInfos, pass.getOverridesCalls); } private static final class CollectTweaksResult { final Map<String, TweakInfo> tweakInfos; final List<TweakFunctionCall> getOverridesCalls; CollectTweaksResult( Map<String, TweakInfo> tweakInfos, List<TweakFunctionCall> getOverridesCalls) { this.tweakInfos = tweakInfos; this.getOverridesCalls = getOverridesCalls; } } /** Processes all calls to goog.tweak functions. */ private final class CollectTweaks extends AbstractPostOrderCallback { final Map<String, TweakInfo> allTweaks = Maps.newHashMap(); final List<TweakFunctionCall> getOverridesCalls = Lists.newArrayList(); @SuppressWarnings("incomplete-switch") @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.isCall()) { return; } String callName = n.getFirstChild().getQualifiedName(); TweakFunction tweakFunc = TWEAK_FUNCTIONS_MAP.get(callName); if (tweakFunc == null) { return; } if (tweakFunc == TweakFunction.GET_COMPILER_OVERRIDES) { getOverridesCalls.add(new TweakFunctionCall(tweakFunc, n)); return; } // Ensure the first parameter (the tweak ID) is a string literal. Node tweakIdNode = n.getFirstChild().getNext(); if (!tweakIdNode.isString()) { compiler.report(t.makeError(tweakIdNode, NON_LITERAL_TWEAK_ID_ERROR)); return; } String tweakId = tweakIdNode.getString(); // Make sure there is a TweakInfo structure for it. TweakInfo tweakInfo = allTweaks.get(tweakId); if (tweakInfo == null) { tweakInfo = new TweakInfo(tweakId); allTweaks.put(tweakId, tweakInfo); } switch (tweakFunc) { case REGISTER_BOOLEAN: case REGISTER_NUMBER: case REGISTER_STRING: // Ensure the ID contains only valid characters. if (!ID_MATCHER.matchesAllOf(tweakId)) { compiler.report(t.makeError(tweakIdNode, INVALID_TWEAK_ID_ERROR)); } // Ensure tweaks are registered in the global scope. if (!t.inGlobalScope()) { compiler.report(t.makeError(n, NON_GLOBAL_TWEAK_INIT_ERROR, tweakId)); break; } // Ensure tweaks are registered only once. if (tweakInfo.isRegistered()) { compiler.report(t.makeError(n, TWEAK_MULTIPLY_REGISTERED_ERROR, tweakId)); break; } Node tweakDefaultValueNode = tweakIdNode.getNext().getNext(); tweakInfo.addRegisterCall(t.getSourceName(), tweakFunc, n, tweakDefaultValueNode); break; case OVERRIDE_DEFAULT_VALUE: // Ensure tweaks overrides occur in the global scope. if (!t.inGlobalScope()) { compiler.report(t.makeError(n, NON_GLOBAL_TWEAK_INIT_ERROR, tweakId)); break; } // Ensure tweak overrides occur before the tweak is registered. if (tweakInfo.isRegistered()) { compiler.report(t.makeError(n, TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR, tweakId)); break; } tweakDefaultValueNode = tweakIdNode.getNext(); tweakInfo.addOverrideDefaultValueCall( t.getSourceName(), tweakFunc, n, tweakDefaultValueNode); break; case GET_BOOLEAN: case GET_NUMBER: case GET_STRING: tweakInfo.addGetterCall(t.getSourceName(), tweakFunc, n); } } } /** Holds information about a call to a goog.tweak function. */ private static final class TweakFunctionCall { final TweakFunction tweakFunc; final Node callNode; final Node valueNode; TweakFunctionCall(TweakFunction tweakFunc, Node callNode) { this(tweakFunc, callNode, null); } TweakFunctionCall(TweakFunction tweakFunc, Node callNode, Node valueNode) { this.callNode = callNode; this.tweakFunc = tweakFunc; this.valueNode = valueNode; } Node getIdNode() { return callNode.getFirstChild().getNext(); } } /** Stores information about a single tweak. */ private final class TweakInfo { final String tweakId; final List<TweakFunctionCall> functionCalls; TweakFunctionCall registerCall; Node defaultValueNode; TweakInfo(String tweakId) { this.tweakId = tweakId; functionCalls = Lists.newArrayList(); } /** * If this tweak is registered, then looks for type warnings in default value parameters and * getter functions. If it is not registered, emits an error for each function call. */ void emitAllWarnings() { if (isRegistered()) { emitAllTypeWarnings(); } else { emitUnknownTweakErrors(); } } /** * Emits a warning for each default value parameter that has the wrong type and for each getter * function that was used for the wrong type of tweak. */ void emitAllTypeWarnings() { for (TweakFunctionCall call : functionCalls) { Node valueNode = call.valueNode; TweakFunction tweakFunc = call.tweakFunc; TweakFunction registerFunc = registerCall.tweakFunc; if (valueNode != null) { // For register* and overrideDefaultValue calls, ensure the default // value is a literal of the correct type. if (!registerFunc.isValidNodeType(valueNode.getType())) { compiler.report( JSError.make( valueNode, INVALID_TWEAK_DEFAULT_VALUE_WARNING, tweakId, registerFunc.getName(), registerFunc.getExpectedTypeName())); } } else if (tweakFunc.isGetterFunction()) { // For getter calls, ensure the correct getter was used. if (!tweakFunc.isCorrectRegisterFunction(registerFunc)) { compiler.report( JSError.make( call.callNode, TWEAK_WRONG_GETTER_TYPE_WARNING, tweakFunc.getName(), registerFunc.getName())); } } } } /** Emits an error for each function call that was found. */ void emitUnknownTweakErrors() { for (TweakFunctionCall call : functionCalls) { compiler.report(JSError.make(call.getIdNode(), UNKNOWN_TWEAK_WARNING, tweakId)); } } void addRegisterCall( String sourceName, TweakFunction tweakFunc, Node callNode, Node defaultValueNode) { registerCall = new TweakFunctionCall(tweakFunc, callNode, defaultValueNode); functionCalls.add(registerCall); } void addOverrideDefaultValueCall( String sourceName, TweakFunction tweakFunc, Node callNode, Node defaultValueNode) { functionCalls.add(new TweakFunctionCall(tweakFunc, callNode, defaultValueNode)); this.defaultValueNode = defaultValueNode; } void addGetterCall(String sourceName, TweakFunction tweakFunc, Node callNode) { functionCalls.add(new TweakFunctionCall(tweakFunc, callNode)); } boolean isRegistered() { return registerCall != null; } Node getDefaultValueNode() { Preconditions.checkState(isRegistered()); // Use calls to goog.tweak.overrideDefaultValue() first. if (defaultValueNode != null) { return defaultValueNode; } // Use the value passed to the register function next. if (registerCall.valueNode != null) { return registerCall.valueNode; } // Otherwise, use the default value for the tweak's type. return registerCall.tweakFunc.createDefaultValueNode(); } } }
/** * Converts ES6 code to valid ES3 code. * * @author [email protected] (Tyler Breisacher) */ public class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapCompilerPass { private final AbstractCompiler compiler; static final DiagnosticType CANNOT_CONVERT = DiagnosticType.error( "JSC_CANNOT_CONVERT", "This code cannot be converted from ES6 to ES3. {0}"); // TODO(tbreisacher): Remove this once all ES6 features are transpilable. static final DiagnosticType CANNOT_CONVERT_YET = DiagnosticType.error( "JSC_CANNOT_CONVERT_YET", "ES6-to-ES3 conversion of ''{0}'' is not yet implemented."); static final DiagnosticType DYNAMIC_EXTENDS_TYPE = DiagnosticType.error( "JSC_DYNAMIC_EXTENDS_TYPE", "The class in an extends clause must be a qualified name."); static final DiagnosticType NO_SUPERTYPE = DiagnosticType.error( "JSC_NO_SUPERTYPE", "The super keyword may only appear in classes with an extends clause."); static final DiagnosticType CLASS_REASSIGNMENT = DiagnosticType.error( "CLASS_REASSIGNMENT", "Class names defined inside a function cannot be reassigned."); // The name of the vars that capture 'this' and 'arguments' // for converting arrow functions. private static final String THIS_VAR = "$jscomp$this"; private static final String ARGUMENTS_VAR = "$jscomp$arguments"; private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args"; private static final String DESTRUCTURING_TEMP_VAR = "$jscomp$destructuring$var"; private int destructuringVarCounter = 0; private static final String FRESH_COMP_PROP_VAR = "$jscomp$compprop"; private static final String ITER_BASE = "$jscomp$iter$"; private static final String ITER_RESULT = "$jscomp$key$"; // These functions are defined in js/es6_runtime.js public static final String COPY_PROP = "$jscomp.copyProperties"; private static final String INHERITS = "$jscomp.inherits"; static final String MAKE_ITER = "$jscomp.makeIterator"; public Es6ToEs3Converter(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverse(compiler, scriptRoot, this); } /** * Some nodes (such as arrow functions) 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.getType()) { case Token.FUNCTION: if (n.isArrowFunction()) { visitArrowFunction(t, n); } break; case Token.CLASS: // Need to check for super references before they get rewritten. checkClassSuperReferences(n); break; case Token.PARAM_LIST: visitParamList(n, parent); break; } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.OBJECTLIT: for (Node child : n.children()) { if (child.isComputedProp()) { visitObjectWithComputedProperty(n, parent); break; } } break; case Token.MEMBER_DEF: if (parent.isObjectLit()) { visitMemberDefInObjectLit(n, parent); } break; case Token.FOR_OF: visitForOf(n, parent); break; case Token.SUPER: visitSuper(n, parent); break; case Token.STRING_KEY: visitStringKey(n); break; case Token.CLASS: for (Node member = n.getLastChild().getFirstChild(); member != null; member = member.getNext()) { if (member.isGetterDef() || member.isSetterDef() || member.getBooleanProp(Node.COMPUTED_PROP_GETTER) || member.getBooleanProp(Node.COMPUTED_PROP_SETTER)) { cannotConvert(member, "getters or setters in class definitions"); return; } } visitClass(n, parent); break; case Token.ARRAYLIT: case Token.NEW: case Token.CALL: for (Node child : n.children()) { if (child.isSpread()) { visitArrayLitOrCallWithSpread(n, parent); break; } } break; case Token.TEMPLATELIT: Es6TemplateLiterals.visitTemplateLiteral(t, n); break; case Token.ARRAY_PATTERN: visitArrayPattern(t, n, parent); break; case Token.OBJECT_PATTERN: visitObjectPattern(t, n, parent); break; } } private void visitObjectPattern(NodeTraversal t, Node objectPattern, Node parent) { Node rhs, nodeToDetach; if (NodeUtil.isNameDeclaration(parent) && !NodeUtil.isEnhancedFor(parent.getParent())) { rhs = objectPattern.getLastChild(); nodeToDetach = parent; } else if (parent.isAssign() && parent.getParent().isExprResult()) { rhs = parent.getLastChild(); nodeToDetach = parent.getParent(); } else if (parent.isStringKey() || parent.isArrayPattern() || parent.isDefaultValue()) { // Nested object pattern; do nothing. We will visit it after rewriting the parent. return; } else if (NodeUtil.isEnhancedFor(parent) || NodeUtil.isEnhancedFor(parent.getParent())) { visitDestructuringPatternInEnhancedFor(objectPattern); return; } else { Preconditions.checkState(parent.isCatch(), parent); cannotConvertYet( objectPattern, "OBJECT_PATTERN that is a child of a " + Token.name(parent.getType())); return; } // Convert 'var {a: b, c: d} = rhs' to: // var temp = rhs; // var b = temp.a; // var d = temp.c; String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); Node tempDecl = IR.var(IR.name(tempVarName), rhs.detachFromParent()) .useSourceInfoIfMissingFromForTree(objectPattern); nodeToDetach.getParent().addChildBefore(tempDecl, nodeToDetach); for (Node child = objectPattern.getFirstChild(), next; child != null; child = next) { next = child.getNext(); Node newLHS, newRHS; if (child.isStringKey()) { Preconditions.checkState(child.hasChildren()); Node getprop = new Node( child.isQuotedString() ? Token.GETELEM : Token.GETPROP, IR.name(tempVarName), IR.string(child.getString())); Node value = child.removeFirstChild(); if (!value.isDefaultValue()) { newLHS = value; newRHS = getprop; } else { newLHS = value.removeFirstChild(); Node defaultValue = value.removeFirstChild(); newRHS = defaultValueHook(getprop, defaultValue); } } else if (child.isComputedProp()) { if (child.getLastChild().isDefaultValue()) { newLHS = child.getLastChild().removeFirstChild(); Node getelem = IR.getelem(IR.name(tempVarName), child.removeFirstChild()); String intermediateTempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); Node intermediateDecl = IR.var(IR.name(intermediateTempVarName), getelem); intermediateDecl.useSourceInfoIfMissingFromForTree(child); nodeToDetach.getParent().addChildBefore(intermediateDecl, nodeToDetach); newRHS = defaultValueHook( IR.name(intermediateTempVarName), child.getLastChild().removeFirstChild()); } else { newRHS = IR.getelem(IR.name(tempVarName), child.removeFirstChild()); newLHS = child.removeFirstChild(); } } else if (child.isDefaultValue()) { newLHS = child.removeFirstChild(); Node defaultValue = child.removeFirstChild(); Node getprop = IR.getprop(IR.name(tempVarName), IR.string(newLHS.getString())); newRHS = defaultValueHook(getprop, defaultValue); } else { throw new IllegalStateException("Unexpected OBJECT_PATTERN child: " + child); } Node newNode; if (NodeUtil.isNameDeclaration(parent)) { newNode = IR.declaration(newLHS, newRHS, parent.getType()); } else if (parent.isAssign()) { newNode = IR.exprResult(IR.assign(newLHS, newRHS)); } else { throw new IllegalStateException("not reached"); } newNode.useSourceInfoIfMissingFromForTree(child); nodeToDetach.getParent().addChildBefore(newNode, nodeToDetach); // Explicitly visit the LHS of the new node since it may be a nested // destructuring pattern. visit(t, newLHS, newLHS.getParent()); } nodeToDetach.detachFromParent(); compiler.reportCodeChange(); } private void visitArrayPattern(NodeTraversal t, Node arrayPattern, Node parent) { Node rhs, nodeToDetach; if (NodeUtil.isNameDeclaration(parent) && !NodeUtil.isEnhancedFor(parent.getParent())) { // The array pattern is the only child, because Es6SplitVariableDeclarations // has already run. Preconditions.checkState(arrayPattern.getNext() == null); rhs = arrayPattern.getLastChild(); nodeToDetach = parent; } else if (parent.isAssign()) { rhs = arrayPattern.getNext(); nodeToDetach = parent.getParent(); Preconditions.checkState(nodeToDetach.isExprResult()); } else if (parent.isArrayPattern() || parent.isDefaultValue() || parent.isStringKey()) { // This is a nested array pattern. Don't do anything now; we'll visit it // after visiting the parent. return; } else if (NodeUtil.isEnhancedFor(parent) || NodeUtil.isEnhancedFor(parent.getParent())) { visitDestructuringPatternInEnhancedFor(arrayPattern); return; } else { Preconditions.checkState(parent.isCatch() || parent.isForOf()); cannotConvertYet( arrayPattern, "ARRAY_PATTERN that is a child of a " + Token.name(parent.getType())); return; } // Convert 'var [x, y] = rhs' to: // var temp = rhs; // var x = temp[0]; // var y = temp[1]; String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); Node tempDecl = IR.var(IR.name(tempVarName), rhs.detachFromParent()) .useSourceInfoIfMissingFromForTree(arrayPattern); nodeToDetach.getParent().addChildBefore(tempDecl, nodeToDetach); int i = 0; for (Node child = arrayPattern.getFirstChild(), next; child != null; child = next, i++) { next = child.getNext(); if (child.isEmpty()) { continue; } Node newLHS, newRHS; if (child.isDefaultValue()) { Node getElem = IR.getelem(IR.name(tempVarName), IR.number(i)); // [x = defaultValue] = rhs; // becomes // var temp = rhs; // x = (temp[0] === undefined) ? defaultValue : temp[0]; newLHS = child.getFirstChild().detachFromParent(); newRHS = defaultValueHook(getElem, child.getLastChild().detachFromParent()); } else if (child.isRest()) { newLHS = child.detachFromParent(); newLHS.setType(Token.NAME); // [].slice.call(temp, i) newRHS = IR.call( IR.getprop(IR.getprop(IR.arraylit(), IR.string("slice")), IR.string("call")), IR.name(tempVarName), IR.number(i)); } else { newLHS = child.detachFromParent(); newRHS = IR.getelem(IR.name(tempVarName), IR.number(i)); } Node newNode; if (parent.isAssign()) { Node assignment = IR.assign(newLHS, newRHS); newNode = IR.exprResult(assignment); } else { newNode = IR.declaration(newLHS, newRHS, parent.getType()); } newNode.useSourceInfoIfMissingFromForTree(arrayPattern); nodeToDetach.getParent().addChildBefore(newNode, nodeToDetach); // Explicitly visit the LHS of the new node since it may be a nested // destructuring pattern. visit(t, newLHS, newLHS.getParent()); } nodeToDetach.detachFromParent(); compiler.reportCodeChange(); } private void visitDestructuringPatternInEnhancedFor(Node pattern) { Node forNode; int declarationType; if (NodeUtil.isEnhancedFor(pattern.getParent())) { forNode = pattern.getParent(); declarationType = Token.ASSIGN; } else { forNode = pattern.getParent().getParent(); declarationType = pattern.getParent().getType(); Preconditions.checkState(NodeUtil.isEnhancedFor(forNode)); } String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); Node block = forNode.getLastChild(); if (declarationType == Token.ASSIGN) { pattern.getParent().replaceChild(pattern, IR.declaration(IR.name(tempVarName), Token.LET)); block.addChildToFront(IR.exprResult(IR.assign(pattern, IR.name(tempVarName)))); } else { pattern.getParent().replaceChild(pattern, IR.name(tempVarName)); block.addChildToFront(IR.declaration(pattern, IR.name(tempVarName), declarationType)); } } /** * Converts a member definition in an object literal to an ES3 key/value pair. Member definitions * in classes are handled in {@link #visitClass}. */ private void visitMemberDefInObjectLit(Node n, Node parent) { String name = n.getString(); Node stringKey = IR.stringKey(name, n.getFirstChild().detachFromParent()); parent.replaceChild(n, stringKey); compiler.reportCodeChange(); } /** Converts extended object literal {a} to {a:a}. */ private void visitStringKey(Node n) { if (!n.hasChildren()) { Node name = IR.name(n.getString()); name.copyInformationFrom(n); n.addChildToBack(name); compiler.reportCodeChange(); } } private void visitForOf(Node node, Node parent) { Node variable = node.removeFirstChild(); Node iterable = node.removeFirstChild(); Node body = node.removeFirstChild(); Node iterName = IR.name(ITER_BASE + compiler.getUniqueNameIdSupplier().get()); Node getNext = IR.call(IR.getprop(iterName.cloneTree(), IR.string("next"))); String variableName = variable.isName() ? variable.getQualifiedName() : variable.getFirstChild().getQualifiedName(); // var or let Node iterResult = IR.name(ITER_RESULT + variableName); Node makeIter = IR.call(NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), MAKE_ITER), iterable); Node init = IR.var(iterName.cloneTree(), makeIter); Node initIterResult = iterResult.cloneTree(); initIterResult.addChildToFront(getNext.cloneTree()); init.addChildToBack(initIterResult); Node cond = IR.not(IR.getprop(iterResult.cloneTree(), IR.string("done"))); Node incr = IR.assign(iterResult.cloneTree(), getNext.cloneTree()); body.addChildToFront( IR.var(IR.name(variableName), IR.getprop(iterResult.cloneTree(), IR.string("value")))); Node newFor = IR.forNode(init, cond, incr, body); newFor.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, newFor); compiler.reportCodeChange(); } private void checkClassReassignment(Node clazz) { Node name = NodeUtil.getClassNameNode(clazz); Node enclosingFunction = getEnclosingFunction(clazz); if (enclosingFunction == null) { return; } CheckClassAssignments checkAssigns = new CheckClassAssignments(name); NodeTraversal.traverse(compiler, enclosingFunction, checkAssigns); } private void visitSuper(Node node, Node parent) { Node enclosing = parent; Node potentialCallee = node; if (!parent.isCall()) { enclosing = parent.getParent(); potentialCallee = parent; } if (!enclosing.isCall() || enclosing.getFirstChild() != potentialCallee) { cannotConvertYet(node, "Only calls to super or to a method of super are supported."); return; } Node clazz = NodeUtil.getEnclosingClass(node); if (clazz == null) { compiler.report(JSError.make(node, NO_SUPERTYPE)); return; } if (NodeUtil.getClassNameNode(clazz) == null) { // Unnamed classes of the form: // f(class extends D { ... }); // give the problem that there is no name to be used in the call to goog.base for the // translation of super calls. // This will throw an error when the class is processed. return; } Node enclosingMemberDef = NodeUtil.getEnclosingClassMember(node); if (enclosingMemberDef.isStaticMember()) { Node superName = clazz.getFirstChild().getNext(); if (!superName.isQualifiedName()) { // This has already been reported, just don't need to continue processing the class. return; } Node callTarget; potentialCallee.detachFromParent(); if (potentialCallee == node) { // of the form super() potentialCallee = IR.getprop(superName.cloneTree(), IR.string(enclosingMemberDef.getString())); enclosing.putBooleanProp(Node.FREE_CALL, false); } else { // of the form super.method() potentialCallee.replaceChild(node, superName.cloneTree()); } callTarget = IR.getprop(potentialCallee, IR.string("call")); enclosing.addChildToFront(callTarget); enclosing.addChildAfter(IR.thisNode(), callTarget); enclosing.useSourceInfoIfMissingFromForTree(enclosing); compiler.reportCodeChange(); return; } String methodName; Node callName = enclosing.removeFirstChild(); if (callName.isSuper()) { methodName = enclosingMemberDef.getString(); } else { methodName = callName.getLastChild().getString(); } Node baseCall = baseCall(clazz, methodName, enclosing.removeChildren()) .useSourceInfoIfMissingFromForTree(enclosing); enclosing.getParent().replaceChild(enclosing, baseCall); compiler.reportCodeChange(); } private Node baseCall(Node clazz, String methodName, Node arguments) { boolean useUnique = NodeUtil.isStatement(clazz) && !isInFunction(clazz); String uniqueClassString = useUnique ? getUniqueClassName(NodeUtil.getClassName(clazz)) : NodeUtil.getClassName(clazz); Node uniqueClassName = NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), uniqueClassString); Node base = IR.getprop(uniqueClassName, IR.string("base")); Node call = IR.call(base, IR.thisNode(), IR.string(methodName)); if (arguments != null) { call.addChildrenToBack(arguments); } return call; } /** Processes trailing default and rest parameters. */ private void visitParamList(Node paramList, Node function) { Node insertSpot = null; Node block = function.getLastChild(); for (int i = 0; i < paramList.getChildCount(); i++) { Node param = paramList.getChildAtIndex(i); if (param.isDefaultValue()) { Node nameOrPattern = param.removeFirstChild(); Node defaultValue = param.removeFirstChild(); Node newParam = nameOrPattern.isName() ? nameOrPattern : IR.name(DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++)); Node lhs = nameOrPattern.cloneTree(); Node rhs = defaultValueHook(newParam.cloneTree(), defaultValue); Node newStatement = nameOrPattern.isName() ? IR.exprResult(IR.assign(lhs, rhs)) : IR.var(lhs, rhs); newStatement.useSourceInfoIfMissingFromForTree(param); block.addChildAfter(newStatement, insertSpot); insertSpot = newStatement; paramList.replaceChild(param, newParam); newParam.setOptionalArg(true); compiler.reportCodeChange(); } else if (param.isRest()) { // rest parameter param.setType(Token.NAME); param.setVarArgs(true); // Transpile to: param = [].slice.call(arguments, i); Node newArr = IR.exprResult( IR.assign( IR.name(param.getString()), IR.call( IR.getprop( IR.getprop(IR.arraylit(), IR.string("slice")), IR.string("call")), IR.name("arguments"), IR.number(i)))); block.addChildAfter(newArr.useSourceInfoIfMissingFromForTree(param), insertSpot); compiler.reportCodeChange(); } else if (param.isDestructuringPattern()) { String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); paramList.replaceChild(param, IR.name(tempVarName)); Node newDecl = IR.var(param, IR.name(tempVarName)); block.addChildAfter(newDecl, insertSpot); insertSpot = newDecl; } } // 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. } /** * Processes array literals or calls containing spreads. Eg.: [1, 2, ...x, 4, 5] => [1, * 2].concat(x, [4, 5]); Eg.: f(...arr) => f.apply(null, arr) Eg.: new F(...args) => new * Function.prototype.bind.apply(F, [].concat(args)) */ private void visitArrayLitOrCallWithSpread(Node node, Node parent) { Preconditions.checkArgument(node.isCall() || node.isArrayLit() || node.isNew()); List<Node> 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(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 joinedGroups = IR.call( IR.getprop(IR.arraylit(), IR.string("concat")), groups.toArray(new Node[groups.size()])); 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 { Node bindApply = NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), "Function.prototype.bind.apply"); result = IR.newNode(bindApply, callee, joinedGroups); } result.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, result); compiler.reportCodeChange(); } private void visitObjectWithComputedProperty(Node obj, Node parent) { Preconditions.checkArgument(obj.isObjectLit()); List<Node> props = new ArrayList<>(); Node currElement = obj.getFirstChild(); while (currElement != null) { if (currElement.getBooleanProp(Node.COMPUTED_PROP_GETTER) || currElement.getBooleanProp(Node.COMPUTED_PROP_SETTER)) { cannotConvertYet(currElement, "computed getter/setter"); return; } else if (currElement.isGetterDef() || currElement.isSetterDef()) { currElement = currElement.getNext(); } else { Node nextNode = currElement.getNext(); obj.removeChild(currElement); props.add(currElement); currElement = nextNode; } } String objName = FRESH_COMP_PROP_VAR + compiler.getUniqueNameIdSupplier().get(); props = Lists.reverse(props); Node result = IR.name(objName); for (Node propdef : props) { if (propdef.isComputedProp()) { Node propertyExpression = propdef.removeFirstChild(); Node value = propdef.removeFirstChild(); result = IR.comma(IR.assign(IR.getelem(IR.name(objName), propertyExpression), value), result); } else { if (!propdef.hasChildren()) { Node name = IR.name(propdef.getString()).copyInformationFrom(propdef); propdef.addChildToBack(name); } Node val = propdef.removeFirstChild(); propdef.setType(Token.STRING); int type = propdef.isQuotedString() ? Token.GETELEM : Token.GETPROP; Node access = new Node(type, IR.name(objName), propdef); result = IR.comma(IR.assign(access, val), result); } } Node statement = obj; while (!NodeUtil.isStatement(statement)) { statement = statement.getParent(); } result.useSourceInfoIfMissingFromForTree(obj); parent.replaceChild(obj, result); Node var = IR.var(IR.name(objName), obj); var.useSourceInfoIfMissingFromForTree(statement); statement.getParent().addChildBefore(var, statement); compiler.reportCodeChange(); } private void checkClassSuperReferences(Node classNode) { Node className = classNode.getFirstChild(); Node superClassName = className.getNext(); if (NodeUtil.referencesSuper(classNode) && !superClassName.isQualifiedName()) { compiler.report(JSError.make(classNode, NO_SUPERTYPE)); } } /** * Classes are processed in 3 phases: 1) The class name is extracted. 2) Class members are * processed and rewritten. 3) The constructor is built. */ private void visitClass(Node classNode, Node parent) { checkClassReassignment(classNode); // Collect Metadata Node className = classNode.getFirstChild(); Node superClassName = className.getNext(); Node classMembers = classNode.getLastChild(); // This is a statement node. We insert methods of the // transpiled class after this node. Node insertionPoint; if (!superClassName.isEmpty() && !superClassName.isQualifiedName()) { compiler.report(JSError.make(superClassName, DYNAMIC_EXTENDS_TYPE)); return; } // The fully qualified name of the class, which will be used in the output. // May come from the class itself or the LHS of an assignment. String fullClassName = null; // Whether the constructor function in the output should be anonymous. boolean anonymous; // If this is a class statement, or a class expression in a simple // assignment or var statement, convert it. In any other case, the // code is too dynamic, so just call cannotConvert. if (NodeUtil.isStatement(classNode)) { fullClassName = className.getString(); anonymous = false; insertionPoint = classNode; } else if (parent.isAssign() && parent.getParent().isExprResult()) { // Add members after the EXPR_RESULT node: // example.C = class {}; example.C.prototype.foo = function() {}; fullClassName = parent.getFirstChild().getQualifiedName(); if (fullClassName == null) { cannotConvert( parent, "Can only convert classes that are declarations or the right hand" + " side of a simple assignment."); return; } anonymous = true; insertionPoint = parent.getParent(); } else if (parent.isName()) { // Add members after the 'var' statement. // var C = class {}; C.prototype.foo = function() {}; fullClassName = parent.getString(); anonymous = true; insertionPoint = parent.getParent(); } else { cannotConvert( parent, "Can only convert classes that are declarations or the right hand" + " side of a simple assignment."); return; } if (!className.isEmpty() && !className.getString().equals(fullClassName)) { // cannot bind two class names in the case of: var Foo = class Bar {}; cannotConvertYet(classNode, "named class in an assignment"); return; } boolean useUnique = NodeUtil.isStatement(classNode) && !isInFunction(classNode); String uniqueFullClassName = useUnique ? getUniqueClassName(fullClassName) : fullClassName; String superClassString = superClassName.getQualifiedName(); Verify.verify(NodeUtil.isStatement(insertionPoint)); Node constructor = null; JSDocInfo ctorJSDocInfo = null; // Process all members of the class for (Node member : classMembers.children()) { if (member.isEmpty()) { continue; } if (member.isMemberDef() && member.getString().equals("constructor")) { ctorJSDocInfo = member.getJSDocInfo(); constructor = member.getFirstChild().detachFromParent(); if (!anonymous) { constructor.replaceChild(constructor.getFirstChild(), className.cloneNode()); } } else { Node qualifiedMemberName; Node method; if (member.isMemberDef()) { if (member.isStaticMember()) { qualifiedMemberName = NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), Joiner.on(".").join(uniqueFullClassName, member.getString())); } else { qualifiedMemberName = NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), Joiner.on(".").join(uniqueFullClassName, "prototype", member.getString())); } method = member.getFirstChild().detachFromParent(); } else if (member.isComputedProp()) { if (member.isStaticMember()) { qualifiedMemberName = IR.getelem( NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), uniqueFullClassName), member.removeFirstChild()); } else { qualifiedMemberName = IR.getelem( NodeUtil.newQualifiedNameNode( compiler.getCodingConvention(), Joiner.on('.').join(uniqueFullClassName, "prototype")), member.removeFirstChild()); } method = member.getLastChild().detachFromParent(); } else { throw new IllegalStateException("Unexpected class member: " + member); } Node assign = IR.assign(qualifiedMemberName, method); assign.useSourceInfoIfMissingFromForTree(member); JSDocInfo info = member.getJSDocInfo(); if (member.isStaticMember() && NodeUtil.referencesThis(assign.getLastChild())) { JSDocInfoBuilder memberDoc; if (info == null) { memberDoc = new JSDocInfoBuilder(true); } else { memberDoc = JSDocInfoBuilder.copyFrom(info); } memberDoc.recordThisType( new JSTypeExpression( new Node(Token.BANG, new Node(Token.QMARK)), member.getSourceFileName())); info = memberDoc.build(assign); } if (info != null) { info.setAssociatedNode(assign); assign.setJSDocInfo(info); } Node newNode = NodeUtil.newExpr(assign); insertionPoint.getParent().addChildAfter(newNode, insertionPoint); insertionPoint = newNode; } } // Rewrite constructor if (constructor == null) { Node body = IR.block(); if (!superClassName.isEmpty()) { Node superCall = baseCall(classNode, "constructor", null); body.addChildToBack(IR.exprResult(superCall)); } Node name = anonymous ? IR.name("").srcref(className) : className.detachFromParent(); constructor = IR.function(name, IR.paramList(), body).useSourceInfoIfMissingFromForTree(classNode); } JSDocInfo classJSDoc = classNode.getJSDocInfo(); JSDocInfoBuilder newInfo = (classJSDoc != null) ? JSDocInfoBuilder.copyFrom(classJSDoc) : new JSDocInfoBuilder(true); newInfo.recordConstructor(); if (!superClassName.isEmpty()) { if (newInfo.isInterfaceRecorded()) { newInfo.recordExtendedInterface( new JSTypeExpression( new Node(Token.BANG, IR.string(superClassString)), superClassName.getSourceFileName())); } else { Node inherits = IR.call( NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), INHERITS), NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), fullClassName), NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), superClassString)); Node inheritsCall = IR.exprResult(inherits); inheritsCall.useSourceInfoIfMissingFromForTree(classNode); Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode); enclosingStatement.getParent().addChildAfter(inheritsCall, enclosingStatement); newInfo.recordBaseType( new JSTypeExpression( new Node(Token.BANG, IR.string(superClassString)), superClassName.getSourceFileName())); Node copyProps = IR.call( NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), COPY_PROP), NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), fullClassName), NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), superClassString)); copyProps.useSourceInfoIfMissingFromForTree(classNode); enclosingStatement .getParent() .addChildAfter(IR.exprResult(copyProps).srcref(classNode), enclosingStatement); } } // Classes are @struct by default. if (!newInfo.isUnrestrictedRecorded() && !newInfo.isDictRecorded() && !newInfo.isStructRecorded()) { newInfo.recordStruct(); } if (ctorJSDocInfo != null) { newInfo.recordSuppressions(ctorJSDocInfo.getSuppressions()); for (String param : ctorJSDocInfo.getParameterNames()) { newInfo.recordParameter(param, ctorJSDocInfo.getParameterType(param)); } } insertionPoint = constructor; if (NodeUtil.isStatement(classNode)) { constructor.getFirstChild().setString(""); Node ctorVar = IR.var(IR.name(fullClassName), constructor); ctorVar.useSourceInfoIfMissingFromForTree(classNode); parent.replaceChild(classNode, ctorVar); } else { parent.replaceChild(classNode, constructor); } if (NodeUtil.isStatement(constructor)) { insertionPoint.setJSDocInfo(newInfo.build(insertionPoint)); } else if (parent.isName()) { // The constructor function is the RHS of a var statement. // Add the JSDoc to the VAR node. Node var = parent.getParent(); var.setJSDocInfo(newInfo.build(var)); } else if (constructor.getParent().isName()) { // Is a newly created VAR node. Node var = constructor.getParent().getParent(); var.setJSDocInfo(newInfo.build(var)); } else if (parent.isAssign()) { // The constructor function is the RHS of an assignment. // Add the JSDoc to the ASSIGN node. parent.setJSDocInfo(newInfo.build(parent)); } else { throw new IllegalStateException("Unexpected parent node " + parent); } compiler.reportCodeChange(); } /** Converts ES6 arrow functions to standard anonymous ES3 functions. */ private void visitArrowFunction(NodeTraversal t, Node n) { n.setIsArrowFunction(false); Node body = n.getLastChild(); if (!body.isBlock()) { body.detachFromParent(); body = IR.block(IR.returnNode(body).srcref(body)).srcref(body); n.addChildToBack(body); } UpdateThisAndArgumentsReferences updater = new UpdateThisAndArgumentsReferences(); NodeTraversal.traverse(compiler, body, updater); addVarDecls(t, updater.changedThis, updater.changedArguments); compiler.reportCodeChange(); } private void addVarDecls(NodeTraversal t, boolean addThis, boolean addArguments) { Scope scope = t.getScope(); if (scope.isDeclared(THIS_VAR, false)) { addThis = false; } if (scope.isDeclared(ARGUMENTS_VAR, false)) { addArguments = false; } Node parent = t.getScopeRoot(); if (parent.isFunction()) { // Add the new node at the beginning of the function body. parent = parent.getLastChild(); } if (parent.isSyntheticBlock() && parent.getFirstChild().isScript()) { // Add the new node inside the SCRIPT node instead of the // synthetic block that contains it. parent = parent.getFirstChild(); } CompilerInput input = compiler.getInput(parent.getInputId()); if (addArguments) { Node name = IR.name(ARGUMENTS_VAR).srcref(parent); Node argumentsVar = IR.var(name, IR.name("arguments").srcref(parent)); argumentsVar.srcref(parent); parent.addChildToFront(argumentsVar); scope.declare(ARGUMENTS_VAR, name, null, input); } if (addThis) { Node name = IR.name(THIS_VAR).srcref(parent); Node thisVar = IR.var(name, IR.thisNode().srcref(parent)); thisVar.srcref(parent); parent.addChildToFront(thisVar); scope.declare(THIS_VAR, name, null, input); } } private static String getUniqueClassName(String qualifiedName) { return qualifiedName; } // TODO(mattloring) move this functionality to scopes once class scopes are computed. private static Node getEnclosingFunction(Node n) { return NodeUtil.getEnclosingType(n, Token.FUNCTION); } private static boolean isInFunction(Node n) { return getEnclosingFunction(n) != null; } /** Helper for transpiling DEFAULT_VALUE trees. */ private static Node defaultValueHook(Node getprop, Node defaultValue) { return IR.hook(IR.sheq(getprop, IR.name("undefined")), defaultValue, getprop.cloneTree()); } private static class UpdateThisAndArgumentsReferences implements NodeTraversal.Callback { private boolean changedThis = false; private boolean changedArguments = false; @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isThis()) { Node name = IR.name(THIS_VAR).srcref(n); parent.replaceChild(n, name); changedThis = true; } else if (n.isName() && n.getString().equals("arguments")) { Node name = IR.name(ARGUMENTS_VAR).srcref(n); parent.replaceChild(n, name); changedArguments = true; } } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return !n.isFunction() || n.isArrowFunction(); } } private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback { private Node className; public CheckClassAssignments(Node className) { this.className = className; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.isAssign() || n.getFirstChild() == className) { return; } if (className.matchesQualifiedName(n.getFirstChild())) { compiler.report(JSError.make(n, CLASS_REASSIGNMENT)); } } } private void cannotConvert(Node n, String message) { compiler.report(JSError.make(n, CANNOT_CONVERT, message)); } /** * Warns the user that the given ES6 feature cannot be converted to ES3 because the transpilation * is not yet implemented. A call to this method is essentially a "TODO(tbreisacher): Implement * {@code feature}" comment. */ private void cannotConvertYet(Node n, String feature) { compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature)); } }
/** * Replace known jQuery aliases and methods with standard conventions so that the compiler * recognizes them. Expected replacements include: - jQuery.fn -> jQuery.prototype - jQuery.extend * -> expanded into direct object assignments - jQuery.expandedEach -> expand into direct * assignments * * @author [email protected] (Chad Killingsworth) */ class ExpandJqueryAliases extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; private final CodingConvention convention; private static final Logger logger = Logger.getLogger(ExpandJqueryAliases.class.getName()); static final DiagnosticType JQUERY_UNABLE_TO_EXPAND_INVALID_LIT_ERROR = DiagnosticType.warning( "JSC_JQUERY_UNABLE_TO_EXPAND_INVALID_LIT", "jQuery.expandedEach call cannot be expanded because the first " + "argument must be an object literal or an array of strings " + "literal."); static final DiagnosticType JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_ERROR = DiagnosticType.error( "JSC_JQUERY_UNABLE_TO_EXPAND_INVALID_NAME", "jQuery.expandedEach expansion would result in the invalid " + "property name \"{0}\"."); static final DiagnosticType JQUERY_USELESS_EACH_EXPANSION = DiagnosticType.warning( "JSC_JQUERY_USELESS_EACH_EXPANSION", "jQuery.expandedEach was not expanded as no valid property " + "assignments were encountered. Consider using jQuery.each instead."); private static final Set<String> JQUERY_EXTEND_NAMES = ImmutableSet.of("jQuery.extend", "jQuery.fn.extend", "jQuery.prototype.extend"); private static final String JQUERY_EXPANDED_EACH_NAME = "jQuery.expandedEach"; private final PeepholeOptimizationsPass peepholePasses; ExpandJqueryAliases(AbstractCompiler compiler) { this.compiler = compiler; this.convention = compiler.getCodingConvention(); // All of the "early" peephole optimizations. // These passes should make the code easier to analyze. // Passes, such as StatementFusion, are omitted for this reason. final boolean late = false; this.peepholePasses = new PeepholeOptimizationsPass( compiler, new PeepholeMinimizeConditions(late), new PeepholeSubstituteAlternateSyntax(late), new PeepholeReplaceKnownMethods(late), new PeepholeRemoveDeadCode(), new PeepholeFoldConstants(late), new PeepholeCollectPropertyAssignments()); } /** * Check that Node n is a call to one of the jQuery.extend methods that we can expand. Valid calls * are single argument calls where the first argument is an object literal or two argument calls * where the first argument is a name and the second argument is an object literal. */ public static boolean isJqueryExtendCall(Node n, String qname, AbstractCompiler compiler) { if (JQUERY_EXTEND_NAMES.contains(qname)) { Node firstArgument = n.getNext(); if (firstArgument == null) { return false; } Node secondArgument = firstArgument.getNext(); if ((firstArgument.isObjectLit() && secondArgument == null) || (firstArgument.isName() || NodeUtil.isGet(firstArgument) && !NodeUtil.mayHaveSideEffects(firstArgument, compiler) && secondArgument != null && secondArgument.isObjectLit() && secondArgument.getNext() == null)) { return true; } } return false; } public boolean isJqueryExpandedEachCall(Node call, String qName) { Preconditions.checkArgument(call.isCall()); return call.getFirstChild() != null && JQUERY_EXPANDED_EACH_NAME.equals(qName); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isGetProp() && convention.isPrototypeAlias(n)) { maybeReplaceJqueryPrototypeAlias(n); } else if (n.isCall()) { Node callTarget = n.getFirstChild(); String qName = callTarget.getQualifiedName(); if (isJqueryExtendCall(callTarget, qName, this.compiler)) { maybeExpandJqueryExtendCall(n); } else if (isJqueryExpandedEachCall(n, qName)) { maybeExpandJqueryEachCall(t, n); } } } @Override public void process(Node externs, Node root) { logger.fine("Expanding Jquery Aliases"); NodeTraversal.traverseEs6(compiler, root, this); } private void maybeReplaceJqueryPrototypeAlias(Node n) { // Check to see if this is the assignment of the original alias. // If so, leave it intact. if (NodeUtil.isLValue(n)) { Node maybeAssign = n.getParent(); while (!NodeUtil.isStatement(maybeAssign) && !maybeAssign.isAssign()) { maybeAssign = maybeAssign.getParent(); } if (maybeAssign.isAssign()) { maybeAssign = maybeAssign.getParent(); if (maybeAssign.isBlock() || maybeAssign.isScript() || NodeUtil.isStatement(maybeAssign)) { return; } } } Node fn = n.getLastChild(); if (fn != null) { n.replaceChild(fn, IR.string("prototype").srcref(fn)); compiler.reportCodeChange(); } } /** * Expand jQuery.extend (and derivative) calls into direct object assignments Example: * jQuery.extend(obj1, {prop1: val1, prop2: val2}) -> obj1.prop1 = val1; obj1.prop2 = val2; */ private void maybeExpandJqueryExtendCall(Node n) { Node callTarget = n.getFirstChild(); Node objectToExtend = callTarget.getNext(); // first argument Node extendArg = objectToExtend.getNext(); // second argument boolean ensureObjectDefined = true; if (extendArg == null) { // Only one argument was specified, so extend jQuery namespace extendArg = objectToExtend; objectToExtend = callTarget.getFirstChild(); ensureObjectDefined = false; } else if (objectToExtend.isGetProp() && (objectToExtend.getLastChild().getString().equals("prototype") || convention.isPrototypeAlias(objectToExtend))) { ensureObjectDefined = false; } // Check for an empty object literal if (!extendArg.hasChildren()) { return; } // Since we are expanding jQuery.extend calls into multiple statements, // encapsulate the new statements in a new block. Node fncBlock = IR.block().srcref(n); if (ensureObjectDefined) { Node assignVal = IR.or(objectToExtend.cloneTree(), IR.objectlit().srcref(n)).srcref(n); Node assign = IR.assign(objectToExtend.cloneTree(), assignVal).srcref(n); fncBlock.addChildrenToFront(IR.exprResult(assign).srcref(n)); } while (extendArg.hasChildren()) { Node currentProp = extendArg.removeFirstChild(); currentProp.setType(Token.STRING); Node propValue = currentProp.removeFirstChild(); Node newProp; if (currentProp.isQuotedString()) { newProp = IR.getelem(objectToExtend.cloneTree(), currentProp).srcref(currentProp); } else { newProp = IR.getprop(objectToExtend.cloneTree(), currentProp).srcref(currentProp); } Node assignNode = IR.assign(newProp, propValue).srcref(currentProp); fncBlock.addChildToBack(IR.exprResult(assignNode).srcref(currentProp)); } // Check to see if the return value is used. If not, replace the original // call with new block. Otherwise, wrap the statements in an // immediately-called anonymous function. if (n.getParent().isExprResult()) { Node parent = n.getParent(); parent.getParent().replaceChild(parent, fncBlock); } else { Node targetVal; if ("jQuery.prototype".equals(objectToExtend.getQualifiedName())) { // When extending the jQuery prototype, return the jQuery namespace. // This is not commonly used. targetVal = objectToExtend.removeFirstChild(); } else { targetVal = objectToExtend.detachFromParent(); } fncBlock.addChildToBack(IR.returnNode(targetVal).srcref(targetVal)); Node fnc = IR.function(IR.name("").srcref(n), IR.paramList().srcref(n), fncBlock).srcref(n); // add an explicit "call" statement so that we can maintain // the same reference for "this" Node newCallTarget = IR.getprop(fnc, IR.string("call").srcref(n)).srcref(n); n.replaceChild(callTarget, newCallTarget); n.putBooleanProp(Node.FREE_CALL, false); // remove any other pre-existing call arguments while (newCallTarget.getNext() != null) { n.removeChildAfter(newCallTarget); } n.addChildToBack(IR.thisNode().srcref(n)); } compiler.reportCodeChange(); } /** * Expand a jQuery.expandedEach call * * <p>Expanded jQuery.expandedEach calls will replace the GETELEM nodes of a property assignment * with GETPROP nodes to allow for renaming. */ private void maybeExpandJqueryEachCall(NodeTraversal t, Node n) { Node objectToLoopOver = n.getChildAtIndex(1); if (objectToLoopOver == null) { return; } Node callbackFunction = objectToLoopOver.getNext(); if (callbackFunction == null || !callbackFunction.isFunction()) { return; } // Run the peephole optimizations on the first argument to handle // cases like ("a " + "b").split(" ") peepholePasses.process(null, n.getChildAtIndex(1)); // Create a reference tree Node nClone = n.cloneTree(); objectToLoopOver = nClone.getChildAtIndex(1); // Check to see if the first argument is something we recognize and can // expand. if (!objectToLoopOver.isObjectLit() && !(objectToLoopOver.isArrayLit() && isArrayLitValidForExpansion(objectToLoopOver))) { t.report(n, JQUERY_UNABLE_TO_EXPAND_INVALID_LIT_ERROR, (String) null); return; } // Find all references to the callback function arguments List<Node> keyNodeReferences = new ArrayList<>(); List<Node> valueNodeReferences = new ArrayList<>(); new NodeTraversal( compiler, new FindCallbackArgumentReferences( callbackFunction, keyNodeReferences, valueNodeReferences, objectToLoopOver.isArrayLit())) .traverseInnerNode( NodeUtil.getFunctionBody(callbackFunction), callbackFunction, t.getScope()); if (keyNodeReferences.isEmpty()) { // We didn't do anything useful ... t.report(n, JQUERY_USELESS_EACH_EXPANSION, (String) null); return; } Node fncBlock = tryExpandJqueryEachCall( t, nClone, callbackFunction, keyNodeReferences, valueNodeReferences); if (fncBlock != null && fncBlock.hasChildren()) { replaceOriginalJqueryEachCall(n, fncBlock); } else { // We didn't do anything useful ... t.report(n, JQUERY_USELESS_EACH_EXPANSION, (String) null); } } private Node tryExpandJqueryEachCall( NodeTraversal t, Node n, Node callbackFunction, List<Node> keyNodes, List<Node> valueNodes) { Node callTarget = n.getFirstChild(); Node objectToLoopOver = callTarget.getNext(); // New block to contain the expanded statements Node fncBlock = IR.block().srcref(callTarget); boolean isValidExpansion = true; // Expand the jQuery.expandedEach call Node key = objectToLoopOver.getFirstChild(), val = null; for (int i = 0; key != null; key = key.getNext(), i++) { if (key != null) { if (objectToLoopOver.isArrayLit()) { // Arrays have a value of their index number val = IR.number(i).srcref(key); } else { val = key.getFirstChild(); } } // Keep track of the replaced nodes so we can reset the tree List<Node> newKeys = new ArrayList<>(); List<Node> newValues = new ArrayList<>(); List<Node> origGetElems = new ArrayList<>(); List<Node> newGetProps = new ArrayList<>(); // Replace all of the key nodes with the prop name for (int j = 0; j < keyNodes.size(); j++) { Node origNode = keyNodes.get(j); Node ancestor = origNode.getParent(); Node newNode = IR.string(key.getString()).srcref(key); newKeys.add(newNode); ancestor.replaceChild(origNode, newNode); // Walk up the tree to see if the key is used in a GETELEM // assignment while (ancestor != null && !NodeUtil.isStatement(ancestor) && !ancestor.isGetElem()) { ancestor = ancestor.getParent(); } // Convert GETELEM nodes to GETPROP nodes so that they can be // renamed or removed. if (ancestor != null && ancestor.isGetElem()) { Node propObject = ancestor; while (propObject.isGetProp() || propObject.isGetElem()) { propObject = propObject.getFirstChild(); } Node ancestorClone = ancestor.cloneTree(); // Run the peephole passes to handle cases such as // obj['lit' + key] = val; peepholePasses.process(null, ancestorClone.getChildAtIndex(1)); Node prop = ancestorClone.getChildAtIndex(1); if (prop.isString() && NodeUtil.isValidPropertyName(LanguageMode.ECMASCRIPT3, prop.getString())) { Node target = ancestorClone.getFirstChild(); Node newGetProp = IR.getprop(target.detachFromParent(), prop.detachFromParent()); newGetProps.add(newGetProp); origGetElems.add(ancestor); ancestor.getParent().replaceChild(ancestor, newGetProp); } else { if (prop.isString() && !NodeUtil.isValidPropertyName(LanguageMode.ECMASCRIPT3, prop.getString())) { t.report(n, JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_ERROR, prop.getString()); } isValidExpansion = false; } } } if (isValidExpansion) { // Replace all of the value nodes with the prop value for (int j = 0; val != null && j < valueNodes.size(); j++) { Node origNode = valueNodes.get(j); Node newNode = val.cloneTree(); newValues.add(newNode); origNode.getParent().replaceChild(origNode, newNode); } // Wrap the new tree in an anonymous function call Node fnc = IR.function( IR.name("").srcref(key), IR.paramList().srcref(key), callbackFunction.getChildAtIndex(2).cloneTree()) .srcref(key); Node call = IR.call(fnc).srcref(key); call.putBooleanProp(Node.FREE_CALL, true); fncBlock.addChildToBack(IR.exprResult(call).srcref(call)); } // Reset the source tree for (int j = 0; j < newGetProps.size(); j++) { newGetProps.get(j).getParent().replaceChild(newGetProps.get(j), origGetElems.get(j)); } for (int j = 0; j < newKeys.size(); j++) { newKeys.get(j).getParent().replaceChild(newKeys.get(j), keyNodes.get(j)); } for (int j = 0; j < newValues.size(); j++) { newValues.get(j).getParent().replaceChild(newValues.get(j), valueNodes.get(j)); } if (!isValidExpansion) { return null; } } return fncBlock; } private void replaceOriginalJqueryEachCall(Node n, Node expandedBlock) { // Check to see if the return value of the original jQuery.expandedEach // call is used. If so, we need to wrap each loop expansion in an anonymous // function and return the original objectToLoopOver. if (n.getParent().isExprResult()) { Node parent = n.getParent(); Node grandparent = parent.getParent(); Node insertAfter = parent; while (expandedBlock.hasChildren()) { Node child = expandedBlock.getFirstChild().detachFromParent(); grandparent.addChildAfter(child, insertAfter); insertAfter = child; } grandparent.removeChild(parent); } else { // Return the original object Node callTarget = n.getFirstChild(); Node objectToLoopOver = callTarget.getNext(); objectToLoopOver.detachFromParent(); Node ret = IR.returnNode(objectToLoopOver).srcref(callTarget); expandedBlock.addChildToBack(ret); // Wrap all of the expanded loop calls in a new anonymous function Node fnc = IR.function( IR.name("").srcref(callTarget), IR.paramList().srcref(callTarget), expandedBlock); n.replaceChild(callTarget, fnc); n.putBooleanProp(Node.FREE_CALL, true); // remove any other pre-existing call arguments while (fnc.getNext() != null) { n.removeChildAfter(fnc); } } compiler.reportCodeChange(); } private boolean isArrayLitValidForExpansion(Node n) { Iterator<Node> iter = n.children().iterator(); while (iter.hasNext()) { Node child = iter.next(); if (!child.isString()) { return false; } } return true; } /** * Given a jQuery.expandedEach callback function, traverse it and collect any references to its * parameter names. */ static class FindCallbackArgumentReferences extends AbstractPostOrderCallback implements ScopedCallback { private final String keyName; private final String valueName; private Scope startingScope; private List<Node> keyReferences; private List<Node> valueReferences; FindCallbackArgumentReferences( Node functionRoot, List<Node> keyReferences, List<Node> valueReferences, boolean useArrayMode) { Preconditions.checkState(functionRoot.isFunction()); String keyString = null, valueString = null; Node callbackParams = NodeUtil.getFunctionParameters(functionRoot); Node param = callbackParams.getFirstChild(); if (param != null) { Preconditions.checkState(param.isName()); keyString = param.getString(); param = param.getNext(); if (param != null) { Preconditions.checkState(param.isName()); valueString = param.getString(); } } this.keyName = keyString; this.valueName = valueString; // For arrays, the keyString is the index number of the element. // We're interested in the value of the element instead if (useArrayMode) { this.keyReferences = valueReferences; this.valueReferences = keyReferences; } else { this.keyReferences = keyReferences; this.valueReferences = valueReferences; } this.startingScope = null; } private boolean isShadowed(String name, Scope scope) { Var nameVar = scope.getVar(name); return nameVar != null && nameVar.getScope() != this.startingScope; } @Override public void visit(NodeTraversal t, Node n, Node parent) { // In the top scope, "this" is a reference to "value" boolean isThis = false; if (t.getScope().getClosestHoistScope() == this.startingScope) { isThis = n.isThis(); } if (isThis || n.isName() && !isShadowed(n.getString(), t.getScope())) { String nodeValue = isThis ? null : n.getString(); if (!isThis && keyName != null && nodeValue.equals(keyName)) { keyReferences.add(n); } else if (isThis || (valueName != null && nodeValue.equals(valueName))) { valueReferences.add(n); } } } /** * As we enter each scope, make sure that the scope doesn't define a local variable with the * same name as our original callback method parameter names. */ @Override public void enterScope(NodeTraversal t) { if (this.startingScope == null) { this.startingScope = t.getScope(); } } @Override public void exitScope(NodeTraversal t) {} } }
/** * Rewrites "Polymer({})" calls into a form that is suitable for type checking and dead code * elimination. Also ensures proper format and types. * * <p>Only works with Polymer version: 0.8 * * @author [email protected] (Jeremy Klein) */ final class PolymerPass extends AbstractPostOrderCallback implements HotSwapCompilerPass { // TODO(jlklein): Switch back to an error when everyone is upgraded to Polymer 1.0 static final DiagnosticType POLYMER_DESCRIPTOR_NOT_VALID = DiagnosticType.warning( "JSC_POLYMER_DESCRIPTOR_NOT_VALID", "The argument to Polymer() is not an obj lit (perhaps because this is a pre-Polymer-1.0 " + "call). Ignoring this call."); static final DiagnosticType POLYMER_INVALID_DECLARATION = DiagnosticType.error( "JSC_POLYMER_INVALID_DECLARAION", "A Polymer() declaration cannot use 'let' or 'const'."); // Errors static final DiagnosticType POLYMER_MISSING_IS = DiagnosticType.error( "JSC_POLYMER_MISSING_IS", "The class descriptor must include an 'is' property."); static final DiagnosticType POLYMER_UNEXPECTED_PARAMS = DiagnosticType.error( "JSC_POLYMER_UNEXPECTED_PARAMS", "The class definition has too many arguments."); static final DiagnosticType POLYMER_MISSING_EXTERNS = DiagnosticType.error("JSC_POLYMER_MISSING_EXTERNS", "Missing Polymer externs."); static final DiagnosticType POLYMER_INVALID_PROPERTY = DiagnosticType.error( "JSC_POLYMER_INVALID_PROPERTY", "Polymer property has an invalid or missing type."); static final DiagnosticType POLYMER_INVALID_BEHAVIOR_ARRAY = DiagnosticType.error( "JSC_POLYMER_INVALID_BEHAVIOR_ARRAY", "The behaviors property must be an array literal."); static final DiagnosticType POLYMER_UNQUALIFIED_BEHAVIOR = DiagnosticType.error( "JSC_POLYMER_UNQUALIFIED_BEHAVIOR", "Behaviors must be global, fully qualified names which are declared as object literals or " + "array literals of other valid Behaviors."); static final DiagnosticType POLYMER_UNANNOTATED_BEHAVIOR = DiagnosticType.error( "JSC_POLYMER_UNANNOTATED_BEHAVIOR", "Behavior declarations must be annotated with @polymerBehavior."); static final DiagnosticType POLYMER_SHORTHAND_NOT_SUPPORTED = DiagnosticType.error( "JSC_POLYMER_SHORTHAND_NOT_SUPPORTED", "Shorthand assignment in object literal is not allowed in " + "Polymer call arguments"); static final String VIRTUAL_FILE = "<PolymerPass.java>"; private final AbstractCompiler compiler; private Node polymerElementExterns; private Set<String> nativeExternsAdded; private final Map<String, String> tagNameMap; private List<Node> polymerElementProps; private final ImmutableSet<String> behaviorNamesNotToCopy; private GlobalNamespace globalNames; public PolymerPass(AbstractCompiler compiler) { this.compiler = compiler; tagNameMap = TagNameToType.getMap(); polymerElementProps = new ArrayList<>(); nativeExternsAdded = new HashSet<>(); behaviorNamesNotToCopy = ImmutableSet.of( "created", "attached", "detached", "attributeChanged", "configure", "ready", "properties", "listeners", "observers", "hostAttributes"); } @Override public void process(Node externs, Node root) { FindPolymerExterns externsCallback = new FindPolymerExterns(); NodeTraversal.traverse(compiler, externs, externsCallback); polymerElementExterns = externsCallback.polymerElementExterns; polymerElementProps = externsCallback.getpolymerElementProps(); if (polymerElementExterns == null) { compiler.report(JSError.make(externs, POLYMER_MISSING_EXTERNS)); return; } globalNames = new GlobalNamespace(compiler, externs, root); hotSwapScript(root, null); } /** Finds the externs for the PolymerElement base class and all of its properties. */ private static class FindPolymerExterns extends AbstractPostOrderCallback { private Node polymerElementExterns; private ImmutableList.Builder<Node> polymerElementProps; private static final String POLYMER_ELEMENT_NAME = "PolymerElement"; public FindPolymerExterns() { polymerElementProps = ImmutableList.builder(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (isPolymerElementExterns(n)) { polymerElementExterns = n; } else if (isPolymerElementPropExpr(n)) { polymerElementProps.add(n); } } /** @return Whether the node is the declaration of PolymerElement. */ private boolean isPolymerElementExterns(Node value) { return value != null && value.isVar() && value.getFirstChild().matchesQualifiedName(POLYMER_ELEMENT_NAME); } /** * @return Whether the node is an expression result of an assignment to a property of * PolymerElement. */ private boolean isPolymerElementPropExpr(Node value) { return value != null && value.isExprResult() && value.getFirstChild().getFirstChild().isGetProp() && NodeUtil.getRootOfQualifiedName(value.getFirstChild().getFirstChild()) .matchesQualifiedName(POLYMER_ELEMENT_NAME); } public List<Node> getpolymerElementProps() { return polymerElementProps.build(); } } /** * For every Polymer Behavior, strip property type annotations and add suppress checktypes on * functions. */ private static class SuppressBehaviors extends AbstractPostOrderCallback { private final AbstractCompiler compiler; public SuppressBehaviors(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (isBehavior(n)) { if (!n.isVar() && !n.isAssign()) { compiler.report(JSError.make(n, POLYMER_UNQUALIFIED_BEHAVIOR)); return; } // Add @nocollapse. JSDocInfoBuilder newDocs = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo()); newDocs.recordNoCollapse(); n.setJSDocInfo(newDocs.build()); Node behaviorValue = n.getChildAtIndex(1); if (n.isVar()) { behaviorValue = n.getFirstChild().getFirstChild(); } suppressBehavior(behaviorValue); } } /** @return Whether the node is the declaration of a Behavior. */ private boolean isBehavior(Node value) { return value.getJSDocInfo() != null && value.getJSDocInfo().isPolymerBehavior(); } /** Strip property type annotations and add suppress checkTypes and globalThis on functions. */ private void suppressBehavior(Node behaviorValue) { if (behaviorValue == null) { compiler.report(JSError.make(behaviorValue, POLYMER_UNQUALIFIED_BEHAVIOR)); return; } if (behaviorValue.isArrayLit()) { for (Node child : behaviorValue.children()) { suppressBehavior(child); } } else if (behaviorValue.isObjectLit()) { stripPropertyTypes(behaviorValue); addBehaviorSuppressions(behaviorValue); } } private void stripPropertyTypes(Node behaviorValue) { List<MemberDefinition> properties = extractProperties(behaviorValue); for (MemberDefinition property : properties) { property.name.removeProp(Node.JSDOC_INFO_PROP); } } private void suppressDefaultValues(Node behaviorValue) { for (MemberDefinition property : extractProperties(behaviorValue)) { if (!property.value.isObjectLit()) { continue; } Node defaultValue = NodeUtil.getFirstPropMatchingKey(property.value, "value"); if (defaultValue == null || !defaultValue.isFunction()) { continue; } Node defaultValueKey = defaultValue.getParent(); JSDocInfoBuilder suppressDoc = JSDocInfoBuilder.maybeCopyFrom(defaultValueKey.getJSDocInfo()); suppressDoc.addSuppression("checkTypes"); suppressDoc.addSuppression("globalThis"); defaultValueKey.setJSDocInfo(suppressDoc.build()); } } private void addBehaviorSuppressions(Node behaviorValue) { for (Node keyNode : behaviorValue.children()) { if (keyNode.getFirstChild().isFunction()) { keyNode.removeProp(Node.JSDOC_INFO_PROP); JSDocInfoBuilder suppressDoc = new JSDocInfoBuilder(true); suppressDoc.addSuppression("checkTypes"); suppressDoc.addSuppression("globalThis"); keyNode.setJSDocInfo(suppressDoc.build()); } } suppressDefaultValues(behaviorValue); } } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverse(compiler, scriptRoot, this); SuppressBehaviors suppressBehaviorsCallback = new SuppressBehaviors(compiler); NodeTraversal.traverse(compiler, scriptRoot, suppressBehaviorsCallback); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (isPolymerCall(n)) { rewriteClassDefinition(n, parent, t); } } private void rewriteClassDefinition(Node n, Node parent, NodeTraversal t) { if (parent.getParent().isConst() || parent.getParent().isLet()) { compiler.report(JSError.make(n, POLYMER_INVALID_DECLARATION)); return; } ClassDefinition def = extractClassDefinition(n); if (def != null) { if (NodeUtil.isNameDeclaration(parent.getParent()) || parent.isAssign()) { rewritePolymerClass(parent.getParent(), def, t); } else { rewritePolymerClass(parent, def, t); } } } private static class MemberDefinition { final JSDocInfo info; final Node name; final Node value; MemberDefinition(JSDocInfo info, Node name, Node value) { this.info = info; this.name = name; this.value = value; } } private static final class BehaviorDefinition { final List<MemberDefinition> props; final List<MemberDefinition> functionsToCopy; final List<MemberDefinition> nonPropertyMembersToCopy; final boolean isGlobalDeclaration; BehaviorDefinition( List<MemberDefinition> props, List<MemberDefinition> functionsToCopy, List<MemberDefinition> nonPropertyMembersToCopy, boolean isGlobalDeclaration) { this.props = props; this.functionsToCopy = functionsToCopy; this.nonPropertyMembersToCopy = nonPropertyMembersToCopy; this.isGlobalDeclaration = isGlobalDeclaration; } } private static final class ClassDefinition { /** The target node (LHS) for the Polymer element definition. */ final Node target; /** The object literal passed to the call to the Polymer() function. */ final Node descriptor; /** The constructor function for the element. */ final MemberDefinition constructor; /** The name of the native HTML element which this element extends. */ final String nativeBaseElement; /** Properties declared in the Polymer "properties" block. */ final List<MemberDefinition> props; /** Flattened list of behavior definitions used by this element. */ final List<BehaviorDefinition> behaviors; ClassDefinition( Node target, Node descriptor, JSDocInfo classInfo, MemberDefinition constructor, String nativeBaseElement, List<MemberDefinition> props, List<BehaviorDefinition> behaviors) { this.target = target; Preconditions.checkState(descriptor.isObjectLit()); this.descriptor = descriptor; this.constructor = constructor; this.nativeBaseElement = nativeBaseElement; this.props = props; this.behaviors = behaviors; } } /** * Validates the class definition and if valid, destructively extracts the class definition from * the AST. */ private ClassDefinition extractClassDefinition(Node callNode) { Node descriptor = NodeUtil.getArgumentForCallOrNew(callNode, 0); if (descriptor == null || !descriptor.isObjectLit()) { // report bad class definition compiler.report(JSError.make(callNode, POLYMER_DESCRIPTOR_NOT_VALID)); return null; } int paramCount = callNode.getChildCount() - 1; if (paramCount != 1) { compiler.report(JSError.make(callNode, POLYMER_UNEXPECTED_PARAMS)); return null; } Node elName = NodeUtil.getFirstPropMatchingKey(descriptor, "is"); if (elName == null) { compiler.report(JSError.make(callNode, POLYMER_MISSING_IS)); return null; } String elNameString = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, elName.getString()); elNameString += "Element"; Node target; if (NodeUtil.isNameDeclaration(callNode.getParent().getParent())) { target = IR.name(callNode.getParent().getString()); } else if (callNode.getParent().isAssign()) { target = callNode.getParent().getFirstChild().cloneTree(); } else { target = IR.name(elNameString); } target.useSourceInfoIfMissingFrom(callNode); JSDocInfo classInfo = NodeUtil.getBestJSDocInfo(target); JSDocInfo ctorInfo = null; Node constructor = NodeUtil.getFirstPropMatchingKey(descriptor, "factoryImpl"); if (constructor == null) { constructor = IR.function(IR.name(""), IR.paramList(), IR.block()); constructor.useSourceInfoFromForTree(callNode); } else { ctorInfo = NodeUtil.getBestJSDocInfo(constructor); } Node baseClass = NodeUtil.getFirstPropMatchingKey(descriptor, "extends"); String nativeBaseElement = baseClass == null ? null : baseClass.getString(); Node behaviorArray = NodeUtil.getFirstPropMatchingKey(descriptor, "behaviors"); List<BehaviorDefinition> behaviors = extractBehaviors(behaviorArray); List<MemberDefinition> allProperties = new LinkedList<>(); for (BehaviorDefinition behavior : behaviors) { overwriteMembersIfPresent(allProperties, behavior.props); } overwriteMembersIfPresent(allProperties, extractProperties(descriptor)); ClassDefinition def = new ClassDefinition( target, descriptor, classInfo, new MemberDefinition(ctorInfo, null, constructor), nativeBaseElement, allProperties, behaviors); return def; } /** * Appends a list of new MemberDefinitions to the end of a list and removes any previous * MemberDefinition in the list which has the same name as the new member. */ private static void overwriteMembersIfPresent( List<MemberDefinition> list, List<MemberDefinition> newMembers) { for (MemberDefinition newMember : newMembers) { for (MemberDefinition member : list) { if (member.name.getString().equals(newMember.name.getString())) { list.remove(member); break; } } list.add(newMember); } } /** * Extracts all Behaviors from an array recursively. The array must be an array literal whose * value is known at compile-time. Entries in the array can be object literals or array literals * (of other behaviors). Behavior names must be global, fully qualified names. * * @see https://github.com/Polymer/polymer/blob/0.8-preview/PRIMER.md#behaviors * @return A list of all {@code BehaviorDefinitions} in the array. */ private List<BehaviorDefinition> extractBehaviors(Node behaviorArray) { if (behaviorArray == null) { return ImmutableList.of(); } if (!behaviorArray.isArrayLit()) { compiler.report(JSError.make(behaviorArray, POLYMER_INVALID_BEHAVIOR_ARRAY)); return ImmutableList.of(); } ImmutableList.Builder<BehaviorDefinition> behaviors = ImmutableList.builder(); for (Node behaviorName : behaviorArray.children()) { if (behaviorName.isObjectLit()) { this.switchDollarSignPropsToBrackets(behaviorName); this.quoteListenerAndHostAttributeKeys(behaviorName); behaviors.add( new BehaviorDefinition( extractProperties(behaviorName), getBehaviorFunctionsToCopy(behaviorName), getNonPropertyMembersToCopy(behaviorName), !NodeUtil.isInFunction(behaviorName))); continue; } Name behaviorGlobalName = globalNames.getSlot(behaviorName.getQualifiedName()); boolean isGlobalDeclaration = true; if (behaviorGlobalName == null) { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); continue; } Ref behaviorDeclaration = behaviorGlobalName.getDeclaration(); // Use any set as a backup declaration, even if it's local. if (behaviorDeclaration == null) { List<Ref> behaviorRefs = behaviorGlobalName.getRefs(); for (Ref ref : behaviorRefs) { if (ref.isSet()) { isGlobalDeclaration = false; behaviorDeclaration = ref; break; } } } if (behaviorDeclaration == null) { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); continue; } Node behaviorDeclarationNode = behaviorDeclaration.getNode(); JSDocInfo behaviorInfo = NodeUtil.getBestJSDocInfo(behaviorDeclarationNode); if (behaviorInfo == null || !behaviorInfo.isPolymerBehavior()) { compiler.report(JSError.make(behaviorDeclarationNode, POLYMER_UNANNOTATED_BEHAVIOR)); } Node behaviorValue = NodeUtil.getRValueOfLValue(behaviorDeclarationNode); if (behaviorValue == null) { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); } else if (behaviorValue.isArrayLit()) { // Individual behaviors can also be arrays of behaviors. Parse them recursively. behaviors.addAll(extractBehaviors(behaviorValue)); } else if (behaviorValue.isObjectLit()) { this.switchDollarSignPropsToBrackets(behaviorValue); this.quoteListenerAndHostAttributeKeys(behaviorValue); behaviors.add( new BehaviorDefinition( extractProperties(behaviorValue), getBehaviorFunctionsToCopy(behaviorValue), getNonPropertyMembersToCopy(behaviorValue), isGlobalDeclaration)); } else { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); } } return behaviors.build(); } /** * @return A list of functions from a behavior which should be copied to the element prototype. */ private List<MemberDefinition> getBehaviorFunctionsToCopy(Node behaviorObjLit) { Preconditions.checkState(behaviorObjLit.isObjectLit()); ImmutableList.Builder<MemberDefinition> functionsToCopy = ImmutableList.builder(); for (Node keyNode : behaviorObjLit.children()) { if ((keyNode.isStringKey() && keyNode.getFirstChild().isFunction() || keyNode.isMemberFunctionDef()) && !behaviorNamesNotToCopy.contains(keyNode.getString())) { functionsToCopy.add( new MemberDefinition( NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild())); } } return functionsToCopy.build(); } /** * @return A list of MemberDefinitions in a behavior which are not in the properties block, but * should still be copied to the element prototype. */ private List<MemberDefinition> getNonPropertyMembersToCopy(Node behaviorObjLit) { Preconditions.checkState(behaviorObjLit.isObjectLit()); ImmutableList.Builder<MemberDefinition> membersToCopy = ImmutableList.builder(); for (Node keyNode : behaviorObjLit.children()) { if (keyNode.isGetterDef() || (keyNode.isStringKey() && !keyNode.getFirstChild().isFunction() && !behaviorNamesNotToCopy.contains(keyNode.getString()))) { membersToCopy.add( new MemberDefinition( NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild())); } } return membersToCopy.build(); } private static List<MemberDefinition> extractProperties(Node descriptor) { Node properties = NodeUtil.getFirstPropMatchingKey(descriptor, "properties"); if (properties == null) { return ImmutableList.of(); } ImmutableList.Builder<MemberDefinition> members = ImmutableList.builder(); for (Node keyNode : properties.children()) { members.add( new MemberDefinition( NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild())); } return members.build(); } private void rewritePolymerClass(Node exprRoot, final ClassDefinition cls, NodeTraversal t) { // Add {@code @lends} to the object literal. Node call = exprRoot.getFirstChild(); if (call.isAssign()) { call = call.getChildAtIndex(1); } else if (call.isName()) { call = call.getFirstChild(); } Node objLit = cls.descriptor; if (hasShorthandAssignment(objLit)) { compiler.report(JSError.make(objLit, POLYMER_SHORTHAND_NOT_SUPPORTED)); return; } JSDocInfoBuilder objLitDoc = new JSDocInfoBuilder(true); objLitDoc.recordLends(cls.target.getQualifiedName() + ".prototype"); objLit.setJSDocInfo(objLitDoc.build()); this.addTypesToFunctions(objLit, cls.target.getQualifiedName()); this.switchDollarSignPropsToBrackets(objLit); this.quoteListenerAndHostAttributeKeys(objLit); // For simplicity add everything into a block, before adding it to the AST. Node block = IR.block(); if (cls.nativeBaseElement != null) { this.appendPolymerElementExterns(cls); } JSDocInfoBuilder constructorDoc = this.getConstructorDoc(cls); // Remove the original constructor JS docs from the objlit. Node ctorKey = cls.constructor.value.getParent(); if (ctorKey != null) { ctorKey.removeProp(Node.JSDOC_INFO_PROP); } if (cls.target.isGetProp()) { // foo.bar = Polymer({...}); Node assign = IR.assign(cls.target.cloneTree(), cls.constructor.value.cloneTree()); assign.setJSDocInfo(constructorDoc.build()); Node exprResult = IR.exprResult(assign); block.addChildToBack(exprResult); } else { // var foo = Polymer({...}); OR Polymer({...}); Node var = IR.var(cls.target.cloneTree(), cls.constructor.value.cloneTree()); var.setJSDocInfo(constructorDoc.build()); block.addChildToBack(var); } appendPropertiesToBlock(cls, block, cls.target.getQualifiedName() + ".prototype."); appendBehaviorMembersToBlock(cls, block); List<MemberDefinition> readOnlyProps = parseReadOnlyProperties(cls, block); addInterfaceExterns(cls, readOnlyProps); removePropertyDocs(objLit); block.useSourceInfoIfMissingFromForTree(exprRoot); Node stmts = block.removeChildren(); Node parent = exprRoot.getParent(); // If the call to Polymer() is not in the global scope and the assignment target is not // namespaced (which likely means it's exported to the global scope), put the type declaration // into the global scope at the start of the current script. // // This avoids unknown type warnings which are a result of the compiler's poor understanding of // types declared inside IIFEs or any non-global scope. We should revisit this decision after // moving to the new type inference system which should be able to infer these types better. if (!t.getScope().isGlobal() && !cls.target.isGetProp()) { Node scriptNode = NodeUtil.getEnclosingScript(exprRoot); scriptNode.addChildrenToFront(stmts); } else { Node beforeRoot = parent.getChildBefore(exprRoot); if (beforeRoot == null) { parent.addChildrenToFront(stmts); } else { parent.addChildrenAfter(stmts, beforeRoot); } } if (exprRoot.isVar()) { Node assignExpr = varToAssign(exprRoot); parent.replaceChild(exprRoot, assignExpr); } compiler.reportCodeChange(); } /** Add an @this annotation to all functions in the objLit. */ private void addTypesToFunctions(Node objLit, String thisType) { Preconditions.checkState(objLit.isObjectLit()); for (Node keyNode : objLit.children()) { Node value = keyNode.getLastChild(); if (value != null && value.isFunction()) { JSDocInfoBuilder fnDoc = JSDocInfoBuilder.maybeCopyFrom(keyNode.getJSDocInfo()); fnDoc.recordThisType( new JSTypeExpression(new Node(Token.BANG, IR.string(thisType)), VIRTUAL_FILE)); keyNode.setJSDocInfo(fnDoc.build()); } } // Add @this and @return to default property values. for (MemberDefinition property : extractProperties(objLit)) { if (!property.value.isObjectLit()) { continue; } if (hasShorthandAssignment(property.value)) { compiler.report(JSError.make(property.value, POLYMER_SHORTHAND_NOT_SUPPORTED)); return; } Node defaultValue = NodeUtil.getFirstPropMatchingKey(property.value, "value"); if (defaultValue == null || !defaultValue.isFunction()) { continue; } Node defaultValueKey = defaultValue.getParent(); JSDocInfoBuilder fnDoc = JSDocInfoBuilder.maybeCopyFrom(defaultValueKey.getJSDocInfo()); fnDoc.recordThisType( new JSTypeExpression(new Node(Token.BANG, IR.string(thisType)), VIRTUAL_FILE)); fnDoc.recordReturnType(getTypeFromProperty(property)); defaultValueKey.setJSDocInfo(fnDoc.build()); } } /** Switches all "this.$.foo" to "this.$['foo']". */ private void switchDollarSignPropsToBrackets(Node objLit) { Preconditions.checkState(objLit.isObjectLit()); for (Node keyNode : objLit.children()) { Node value = keyNode.getFirstChild(); if (value != null && value.isFunction()) { NodeUtil.visitPostOrder( value.getLastChild(), new NodeUtil.Visitor() { @Override public void visit(Node n) { if (n.isString() && n.getString().equals("$") && n.getParent().isGetProp() && n.getParent().getParent().isGetProp()) { Node dollarChildProp = n.getParent().getParent(); dollarChildProp.setType(Token.GETELEM); compiler.reportCodeChange(); } } }, Predicates.<Node>alwaysTrue()); } } } /** * Makes sure that the keys for listeners and hostAttributes blocks are quoted to avoid renaming. */ private void quoteListenerAndHostAttributeKeys(Node objLit) { Preconditions.checkState(objLit.isObjectLit()); for (Node keyNode : objLit.children()) { if (keyNode.isComputedProp()) { continue; } if (!keyNode.getString().equals("listeners") && !keyNode.getString().equals("hostAttributes")) { continue; } for (Node keyToQuote : keyNode.getFirstChild().children()) { keyToQuote.setQuotedString(); } } } /** Appends all properties in the ClassDefinition to the prototype of the custom element. */ private void appendPropertiesToBlock(final ClassDefinition cls, Node block, String basePath) { for (MemberDefinition prop : cls.props) { Node propertyNode = IR.exprResult(NodeUtil.newQName(compiler, basePath + prop.name.getString())); JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(prop.info); JSTypeExpression propType = getTypeFromProperty(prop); if (propType == null) { return; } info.recordType(propType); propertyNode.getFirstChild().setJSDocInfo(info.build()); block.addChildToBack(propertyNode); } } /** Remove all JSDocs from properties of a class definition */ private void removePropertyDocs(final Node objLit) { for (MemberDefinition prop : extractProperties(objLit)) { prop.name.removeProp(Node.JSDOC_INFO_PROP); } } /** Appends all required behavior functions and non-property members to the given block. */ private void appendBehaviorMembersToBlock(final ClassDefinition cls, Node block) { String qualifiedPath = cls.target.getQualifiedName() + ".prototype."; Map<String, Node> nameToExprResult = new HashMap<>(); for (BehaviorDefinition behavior : cls.behaviors) { for (MemberDefinition behaviorFunction : behavior.functionsToCopy) { String fnName = behaviorFunction.name.getString(); // Don't copy functions already defined by the element itself. if (NodeUtil.getFirstPropMatchingKey(cls.descriptor, fnName) != null) { continue; } // Avoid copying over the same function twice. The last definition always wins. if (nameToExprResult.containsKey(fnName)) { block.removeChild(nameToExprResult.get(fnName)); } Node fnValue = behaviorFunction.value.cloneTree(); Node exprResult = IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + fnName), fnValue)); JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(behaviorFunction.info); // Behaviors whose declarations are not in the global scope may contain references to // symbols which do not exist in the element's scope. Only copy a function stub. See if (!behavior.isGlobalDeclaration) { NodeUtil.getFunctionBody(fnValue).removeChildren(); } exprResult.getFirstChild().setJSDocInfo(info.build()); block.addChildToBack(exprResult); nameToExprResult.put(fnName, exprResult); } // Copy other members. for (MemberDefinition behaviorProp : behavior.nonPropertyMembersToCopy) { String propName = behaviorProp.name.getString(); if (nameToExprResult.containsKey(propName)) { block.removeChild(nameToExprResult.get(propName)); } Node exprResult = IR.exprResult(NodeUtil.newQName(compiler, qualifiedPath + propName)); JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(behaviorProp.info); if (behaviorProp.name.isGetterDef()) { info = new JSDocInfoBuilder(true); if (behaviorProp.info != null && behaviorProp.info.getReturnType() != null) { info.recordType(behaviorProp.info.getReturnType()); } } exprResult.getFirstChild().setJSDocInfo(info.build()); block.addChildToBack(exprResult); nameToExprResult.put(propName, exprResult); } } } /** * Generates the _set* setters for readonly properties and appends them to the given block. * * @return A List of all readonly properties. */ private List<MemberDefinition> parseReadOnlyProperties(final ClassDefinition cls, Node block) { String qualifiedPath = cls.target.getQualifiedName() + ".prototype."; ImmutableList.Builder<MemberDefinition> readOnlyProps = ImmutableList.builder(); for (MemberDefinition prop : cls.props) { // Generate the setter for readOnly properties. if (prop.value.isObjectLit()) { Node readOnlyValue = NodeUtil.getFirstPropMatchingKey(prop.value, "readOnly"); if (readOnlyValue != null && readOnlyValue.isTrue()) { block.addChildToBack(makeReadOnlySetter(prop.name.getString(), qualifiedPath)); readOnlyProps.add(prop); } } } return readOnlyProps.build(); } /** * Gets the JSTypeExpression for a given property using its "type" key. * * @see https://github.com/Polymer/polymer/blob/0.8-preview/PRIMER.md#configuring-properties */ private JSTypeExpression getTypeFromProperty(MemberDefinition property) { if (property.info != null && property.info.hasType()) { return property.info.getType(); } String typeString = ""; if (property.value.isObjectLit()) { Node typeValue = NodeUtil.getFirstPropMatchingKey(property.value, "type"); if (typeValue == null || !typeValue.isName()) { compiler.report(JSError.make(property.name, POLYMER_INVALID_PROPERTY)); return null; } typeString = typeValue.getString(); } else if (property.value.isName()) { typeString = property.value.getString(); } Node typeNode = null; switch (typeString) { case "Boolean": case "String": case "Number": typeNode = IR.string(typeString.toLowerCase()); break; case "Array": case "Function": case "Object": case "Date": typeNode = new Node(Token.BANG, IR.string(typeString)); break; default: compiler.report(JSError.make(property.name, POLYMER_INVALID_PROPERTY)); return null; } return new JSTypeExpression(typeNode, VIRTUAL_FILE); } /** * Adds the generated setter for a readonly property. * * @see https://www.polymer-project.org/0.8/docs/devguide/properties.html#read-only */ private Node makeReadOnlySetter(String propName, String qualifiedPath) { String setterName = "_set" + propName.substring(0, 1).toUpperCase() + propName.substring(1); Node fnNode = IR.function(IR.name(""), IR.paramList(IR.name(propName)), IR.block()); Node exprResNode = IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + setterName), fnNode)); JSDocInfoBuilder info = new JSDocInfoBuilder(true); // This is overriding a generated function which was added to the interface in // {@code addInterfaceExterns}. info.recordOverride(); exprResNode.getFirstChild().setJSDocInfo(info.build()); return exprResNode; } /** * Duplicates the PolymerElement externs with a different element base class if needed. For * example, if the base class is HTMLInputElement, then a class PolymerInputElement will be added. * If the element does not extend a native HTML element, this method is a no-op. */ private void appendPolymerElementExterns(final ClassDefinition cls) { if (!nativeExternsAdded.add(cls.nativeBaseElement)) { return; } Node block = IR.block(); Node baseExterns = polymerElementExterns.cloneTree(); String polymerElementType = getPolymerElementType(cls); baseExterns.getFirstChild().setString(polymerElementType); String elementType = tagNameMap.get(cls.nativeBaseElement); JSTypeExpression elementBaseType = new JSTypeExpression(new Node(Token.BANG, IR.string(elementType)), VIRTUAL_FILE); JSDocInfoBuilder baseDocs = JSDocInfoBuilder.copyFrom(baseExterns.getJSDocInfo()); baseDocs.changeBaseType(elementBaseType); baseExterns.setJSDocInfo(baseDocs.build()); block.addChildToBack(baseExterns); for (Node baseProp : polymerElementProps) { Node newProp = baseProp.cloneTree(); Node newPropRootName = NodeUtil.getRootOfQualifiedName(newProp.getFirstChild().getFirstChild()); newPropRootName.setString(polymerElementType); block.addChildToBack(newProp); } Node parent = polymerElementExterns.getParent(); Node stmts = block.removeChildren(); parent.addChildrenAfter(stmts, polymerElementExterns); compiler.reportCodeChange(); } /** * Adds an interface for the given ClassDefinition to externs. This allows generated setter * functions for read-only properties to avoid renaming altogether. * * @see https://www.polymer-project.org/0.8/docs/devguide/properties.html#read-only */ private void addInterfaceExterns( final ClassDefinition cls, List<MemberDefinition> readOnlyProps) { Node block = IR.block(); String interfaceName = getInterfaceName(cls); Node fnNode = IR.function(IR.name(""), IR.paramList(), IR.block()); Node varNode = IR.var(NodeUtil.newQName(compiler, interfaceName), fnNode); JSDocInfoBuilder info = new JSDocInfoBuilder(true); info.recordInterface(); varNode.setJSDocInfo(info.build()); block.addChildToBack(varNode); appendPropertiesToBlock(cls, block, interfaceName + ".prototype."); for (MemberDefinition prop : readOnlyProps) { // Add all _set* functions to avoid renaming. String propName = prop.name.getString(); String setterName = "_set" + propName.substring(0, 1).toUpperCase() + propName.substring(1); Node setterExprNode = IR.exprResult(NodeUtil.newQName(compiler, interfaceName + ".prototype." + setterName)); JSDocInfoBuilder setterInfo = new JSDocInfoBuilder(true); JSTypeExpression propType = getTypeFromProperty(prop); setterInfo.recordParameter(propName, propType); setterExprNode.getFirstChild().setJSDocInfo(setterInfo.build()); block.addChildToBack(setterExprNode); } Node parent = polymerElementExterns.getParent(); Node stmts = block.removeChildren(); parent.addChildrenToBack(stmts); compiler.reportCodeChange(); } /** @return The name of the generated extern interface which the element implements. */ private String getInterfaceName(final ClassDefinition cls) { return "Polymer" + cls.target.getQualifiedName().replaceAll("\\.", "_") + "Interface"; } /** @return The proper constructor doc for the Polymer call. */ private JSDocInfoBuilder getConstructorDoc(final ClassDefinition cls) { JSDocInfoBuilder constructorDoc = JSDocInfoBuilder.maybeCopyFrom(cls.constructor.info); constructorDoc.recordConstructor(); JSTypeExpression baseType = new JSTypeExpression( new Node(Token.BANG, IR.string(getPolymerElementType(cls))), VIRTUAL_FILE); constructorDoc.recordBaseType(baseType); String interfaceName = getInterfaceName(cls); JSTypeExpression interfaceType = new JSTypeExpression(new Node(Token.BANG, IR.string(interfaceName)), VIRTUAL_FILE); constructorDoc.recordImplementedInterface(interfaceType); return constructorDoc; } /** @return An assign replacing the equivalent var declaration. */ private static Node varToAssign(Node var) { Node assign = IR.assign(IR.name(var.getFirstChild().getString()), var.getFirstChild().removeFirstChild()); return IR.exprResult(assign).useSourceInfoFromForTree(var); } /** @return The PolymerElement type string for a class definition. */ private static String getPolymerElementType(final ClassDefinition cls) { return String.format( "Polymer%sElement", cls.nativeBaseElement == null ? "" : CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, cls.nativeBaseElement)); } /** @return Whether the call represents a call to Polymer. */ private static boolean isPolymerCall(Node value) { return value != null && value.isCall() && value.getFirstChild().matchesQualifiedName("Polymer"); } private boolean hasShorthandAssignment(Node objLit) { Preconditions.checkState(objLit.isObjectLit()); for (Node property : objLit.children()) { if (property.isStringKey() && !property.hasChildren()) { return true; } } return false; } }
/** * Checks that the code obeys the static restrictions of strict mode: * * <ol> * <li>No use of "with". * <li>No deleting variables, functions, or arguments. * <li>No re-declarations or assignments of "eval" or arguments. * <li>No use of arguments.callee * <li>No use of arguments.caller * <li>Class: Always under strict mode * <li>In addition, no duplicate class method names * </ol> */ class StrictModeCheck extends AbstractPostOrderCallback implements CompilerPass { static final DiagnosticType USE_OF_WITH = DiagnosticType.warning( "JSC_USE_OF_WITH", "The 'with' statement cannot be used in ES5 strict mode."); static final DiagnosticType EVAL_DECLARATION = DiagnosticType.warning( "JSC_EVAL_DECLARATION", "\"eval\" cannot be redeclared in ES5 strict mode"); static final DiagnosticType EVAL_ASSIGNMENT = DiagnosticType.warning( "JSC_EVAL_ASSIGNMENT", "the \"eval\" object cannot be reassigned in ES5 strict mode"); static final DiagnosticType ARGUMENTS_DECLARATION = DiagnosticType.warning( "JSC_ARGUMENTS_DECLARATION", "\"arguments\" cannot be redeclared in ES5 strict mode"); static final DiagnosticType ARGUMENTS_ASSIGNMENT = DiagnosticType.warning( "JSC_ARGUMENTS_ASSIGNMENT", "the \"arguments\" object cannot be reassigned in ES5 strict mode"); static final DiagnosticType ARGUMENTS_CALLEE_FORBIDDEN = DiagnosticType.warning( "JSC_ARGUMENTS_CALLEE_FORBIDDEN", "\"arguments.callee\" cannot be used in ES5 strict mode"); static final DiagnosticType ARGUMENTS_CALLER_FORBIDDEN = DiagnosticType.warning( "JSC_ARGUMENTS_CALLER_FORBIDDEN", "\"arguments.caller\" cannot be used in ES5 strict mode"); static final DiagnosticType FUNCTION_CALLER_FORBIDDEN = DiagnosticType.warning( "JSC_FUNCTION_CALLER_FORBIDDEN", "A function''s \"caller\" property cannot be used in ES5 strict mode"); static final DiagnosticType FUNCTION_ARGUMENTS_PROP_FORBIDDEN = DiagnosticType.warning( "JSC_FUNCTION_ARGUMENTS_PROP_FORBIDDEN", "A function''s \"arguments\" property cannot be used in ES5 strict mode"); static final DiagnosticType DELETE_VARIABLE = DiagnosticType.warning( "JSC_DELETE_VARIABLE", "variables, functions, and arguments cannot be deleted in " + "ES5 strict mode"); static final DiagnosticType DUPLICATE_OBJECT_KEY = DiagnosticType.warning( "JSC_DUPLICATE_OBJECT_KEY", "object literals cannot contain duplicate keys in ES5 strict mode"); static final DiagnosticType DUPLICATE_CLASS_METHODS = DiagnosticType.error( "JSC_DUPLICATE_CLASS_METHODS", "Classes cannot contain duplicate method names"); static final DiagnosticType BAD_FUNCTION_DECLARATION = DiagnosticType.error( "JSC_BAD_FUNCTION_DECLARATION", "functions can only be declared at top level or immediately within " + "another function in ES5 strict mode"); private final AbstractCompiler compiler; private final boolean noVarCheck; StrictModeCheck(AbstractCompiler compiler) { this(compiler, false); } StrictModeCheck(AbstractCompiler compiler, boolean noVarCheck) { this.compiler = compiler; this.noVarCheck = noVarCheck; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseRoots(compiler, this, externs, root); NodeTraversal.traverseEs6(compiler, root, new NonExternChecks()); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isFunction()) { checkFunctionUse(t, n); } else if (n.isAssign()) { checkAssignment(t, n); } else if (n.isDelProp()) { checkDelete(t, n); } else if (n.isObjectLit()) { checkObjectLiteralOrClass(t, n); } else if (n.isClass()) { checkObjectLiteralOrClass(t, n.getLastChild()); } else if (n.isWith()) { checkWith(t, n); } } /** Reports a warning for with statements. */ private static void checkWith(NodeTraversal t, Node n) { JSDocInfo info = n.getJSDocInfo(); boolean allowWith = info != null && info.getSuppressions().contains("with"); if (!allowWith) { t.report(n, USE_OF_WITH); } } /** Checks that the function is used legally. */ private static void checkFunctionUse(NodeTraversal t, Node n) { if (NodeUtil.isFunctionDeclaration(n) && !NodeUtil.isHoistedFunctionDeclaration(n)) { t.report(n, BAD_FUNCTION_DECLARATION); } } /** * Determines if the given name is a declaration, which can be a declaration of a variable, * function, or argument. */ private static boolean isDeclaration(Node n) { switch (n.getParent().getType()) { case Token.LET: case Token.CONST: case Token.VAR: case Token.FUNCTION: case Token.CATCH: return true; case Token.PARAM_LIST: return n.getParent().getParent().isFunction(); default: return false; } } /** Checks that an assignment is not to the "arguments" object. */ private static void checkAssignment(NodeTraversal t, Node n) { if (n.getFirstChild().isName()) { if ("arguments".equals(n.getFirstChild().getString())) { t.report(n, ARGUMENTS_ASSIGNMENT); } else if ("eval".equals(n.getFirstChild().getString())) { // Note that assignment to eval is already illegal because any use of // that name is illegal. t.report(n, EVAL_ASSIGNMENT); } } } /** Checks that variables, functions, and arguments are not deleted. */ private static void checkDelete(NodeTraversal t, Node n) { if (n.getFirstChild().isName()) { Var v = t.getScope().getVar(n.getFirstChild().getString()); if (v != null) { t.report(n, DELETE_VARIABLE); } } } /** Checks that object literal keys or class method names are valid. */ private static void checkObjectLiteralOrClass(NodeTraversal t, Node n) { Set<String> getters = new HashSet<>(); Set<String> setters = new HashSet<>(); for (Node key = n.getFirstChild(); key != null; key = key.getNext()) { if (!key.isSetterDef()) { // normal property and getter cases if (!getters.add(key.getString())) { if (n.isClassMembers()) { t.report(key, DUPLICATE_CLASS_METHODS); } else { t.report(key, DUPLICATE_OBJECT_KEY); } } } if (!key.isGetterDef()) { // normal property and setter cases if (!setters.add(key.getString())) { if (n.isClassMembers()) { t.report(key, DUPLICATE_CLASS_METHODS); } else { t.report(key, DUPLICATE_OBJECT_KEY); } } } } } /** Checks that are performed on non-extern code only. */ private static class NonExternChecks extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if ((n.isName()) && isDeclaration(n)) { checkDeclaration(t, n); } else if (n.isGetProp()) { checkGetProp(t, n); } } /** Checks for illegal declarations. */ private void checkDeclaration(NodeTraversal t, Node n) { if ("eval".equals(n.getString())) { t.report(n, EVAL_DECLARATION); } else if ("arguments".equals(n.getString())) { t.report(n, ARGUMENTS_DECLARATION); } } /** Checks that the arguments.callee is not used. */ private void checkGetProp(NodeTraversal t, Node n) { Node target = n.getFirstChild(); Node prop = n.getLastChild(); if (prop.getString().equals("callee")) { if (target.isName() && target.getString().equals("arguments")) { t.report(n, ARGUMENTS_CALLEE_FORBIDDEN); } } else if (prop.getString().equals("caller")) { if (target.isName() && target.getString().equals("arguments")) { t.report(n, ARGUMENTS_CALLER_FORBIDDEN); } else if (isFunctionType(target)) { t.report(n, FUNCTION_CALLER_FORBIDDEN); } } else if (prop.getString().equals("arguments") && isFunctionType(target)) { t.report(n, FUNCTION_ARGUMENTS_PROP_FORBIDDEN); } } } private static boolean isFunctionType(Node n) { TypeI type = n.getTypeI(); return (type != null && type.isFunctionType()); } }
/** * NodeTraversal allows an iteration through the nodes in the parse tree, and facilitates the * optimizations on the parse tree. */ public class NodeTraversal { private final AbstractCompiler compiler; private final Callback callback; /** Contains the current node */ private Node curNode; public static final DiagnosticType NODE_TRAVERSAL_ERROR = DiagnosticType.error("JSC_NODE_TRAVERSAL_ERROR", "{0}"); /** * Stack containing the Scopes that have been created. The Scope objects are lazily created; so * the {@code scopeRoots} stack contains the Nodes for all Scopes that have not been created yet. */ private final Deque<Scope> scopes = new ArrayDeque<>(); /** * A stack of scope roots. All scopes that have not been created are represented in this Deque. */ private final Deque<Node> scopeRoots = new ArrayDeque<>(); /** * A stack of scope roots that are valid cfg roots. All cfg roots that have not been created are * represented in this Deque. */ private final Deque<Node> cfgRoots = new ArrayDeque<>(); /** * Stack of control flow graphs (CFG). There is one CFG per scope. CFGs are lazily populated: * elements are {@code null} until requested by {@link #getControlFlowGraph()}. Note that {@link * ArrayDeque} does not allow {@code null} elements, so {@link LinkedList} is used instead. */ Deque<ControlFlowGraph<Node>> cfgs = new LinkedList<>(); /** The current source file name */ private String sourceName; /** The current input */ private InputId inputId; /** The scope creator */ private final ScopeCreator scopeCreator; private final boolean useBlockScope; /** Possible callback for scope entry and exist * */ private ScopedCallback scopeCallback; /** Callback for passes that iterate over a list of functions */ public interface FunctionCallback { void visit(AbstractCompiler compiler, Node fnRoot); } /** Callback for tree-based traversals */ public interface Callback { /** * Visits a node in pre order (before visiting its children) and decides whether this node's * children should be traversed. If children are traversed, they will be visited by {@link * #visit(NodeTraversal, Node, Node)} in postorder. * * <p>Implementations can have side effects (e.g. modifying the parse tree). * * @return whether the children of this node should be visited */ boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent); /** * Visits a node in postorder (after its children have been visited). A node is visited only if * all its parents should be traversed ({@link #shouldTraverse(NodeTraversal, Node, Node)}). * * <p>Implementations can have side effects (e.g. modifying the parse tree). */ void visit(NodeTraversal t, Node n, Node parent); } /** Callback that also knows about scope changes */ public interface ScopedCallback extends Callback { /** * Called immediately after entering a new scope. The new scope can be accessed through * t.getScope() */ void enterScope(NodeTraversal t); /** * Called immediately before exiting a scope. The ending scope can be accessed through * t.getScope() */ void exitScope(NodeTraversal t); } /** Abstract callback to visit all nodes in postorder. */ public abstract static class AbstractPostOrderCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } } /** Abstract callback to visit all nodes in preorder. */ public abstract static class AbstractPreOrderCallback implements Callback { @Override public void visit(NodeTraversal t, Node n, Node parent) {} } /** Abstract scoped callback to visit all nodes in postorder. */ public abstract static class AbstractScopedCallback implements ScopedCallback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void enterScope(NodeTraversal t) {} @Override public void exitScope(NodeTraversal t) {} } /** Abstract callback to visit all nodes but not traverse into function bodies. */ public abstract static class AbstractShallowCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { // We do want to traverse the name of a named function, but we don't // want to traverse the arguments or body. return parent == null || !parent.isFunction() || n == parent.getFirstChild(); } } /** * Abstract callback to visit all structure and statement nodes but doesn't traverse into * functions or expressions. */ public abstract static class AbstractShallowStatementCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return parent == null || NodeUtil.isControlStructure(parent) || NodeUtil.isStatementBlock(parent); } } /** Abstract callback to visit a pruned set of nodes. */ public abstract static class AbstractNodeTypePruningCallback implements Callback { private final Set<Integer> nodeTypes; private final boolean include; /** * Creates an abstract pruned callback. * * @param nodeTypes the nodes to include in the traversal */ public AbstractNodeTypePruningCallback(Set<Integer> nodeTypes) { this(nodeTypes, true); } /** * Creates an abstract pruned callback. * * @param nodeTypes the nodes to include/exclude in the traversal * @param include whether to include or exclude the nodes in the traversal */ public AbstractNodeTypePruningCallback(Set<Integer> nodeTypes, boolean include) { this.nodeTypes = nodeTypes; this.include = include; } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return include == nodeTypes.contains(n.getType()); } } /** Creates a node traversal using the specified callback interface. */ public NodeTraversal(AbstractCompiler compiler, Callback cb) { this( compiler, cb, compiler.getLanguageMode().isEs6OrHigher() ? new Es6SyntacticScopeCreator(compiler) : SyntacticScopeCreator.makeUntyped(compiler)); } /** Creates a node traversal using the specified callback interface and the scope creator. */ public NodeTraversal(AbstractCompiler compiler, Callback cb, ScopeCreator scopeCreator) { this.callback = cb; if (cb instanceof ScopedCallback) { this.scopeCallback = (ScopedCallback) cb; } this.compiler = compiler; this.inputId = null; this.sourceName = ""; this.scopeCreator = scopeCreator; this.useBlockScope = scopeCreator.hasBlockScope(); } private void throwUnexpectedException(Exception unexpectedException) { // If there's an unexpected exception, try to get the // line number of the code that caused it. String message = unexpectedException.getMessage(); // TODO(user): It is possible to get more information if curNode or // its parent is missing. We still have the scope stack in which it is still // very useful to find out at least which function caused the exception. if (inputId != null) { message = unexpectedException.getMessage() + "\n" + formatNodeContext("Node", curNode) + (curNode == null ? "" : formatNodeContext("Parent", curNode.getParent())); } compiler.throwInternalError(message, unexpectedException); } private String formatNodeContext(String label, Node n) { if (n == null) { return " " + label + ": NULL"; } return " " + label + "(" + n.toString(false, false, false) + "): " + formatNodePosition(n); } /** Traverses a parse tree recursively. */ public void traverse(Node root) { try { inputId = NodeUtil.getInputId(root); sourceName = ""; curNode = root; pushScope(root); // null parent ensures that the shallow callbacks will traverse root traverseBranch(root, null); popScope(); } catch (Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } void traverseRoots(Node externs, Node root) { try { Node scopeRoot = externs.getParent(); Preconditions.checkState(scopeRoot != null); inputId = NodeUtil.getInputId(scopeRoot); sourceName = ""; curNode = scopeRoot; pushScope(scopeRoot); traverseBranch(externs, scopeRoot); Preconditions.checkState(root.getParent() == scopeRoot); traverseBranch(root, scopeRoot); popScope(); } catch (Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } private static final String MISSING_SOURCE = "[source unknown]"; private String formatNodePosition(Node n) { String sourceFileName = getBestSourceFileName(n); if (sourceFileName == null) { return MISSING_SOURCE + "\n"; } int lineNumber = n.getLineno(); int columnNumber = n.getCharno(); String src = compiler.getSourceLine(sourceFileName, lineNumber); if (src == null) { src = MISSING_SOURCE; } return sourceFileName + ":" + lineNumber + ":" + columnNumber + "\n" + src + "\n"; } /** * Traverses a parse tree recursively with a scope, starting with the given root. This should only * be used in the global scope. Otherwise, use {@link #traverseAtScope}. */ void traverseWithScope(Node root, Scope s) { Preconditions.checkState(s.isGlobal()); try { inputId = null; sourceName = ""; curNode = root; pushScope(s); traverseBranch(root, null); popScope(); } catch (Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } /** Traverses a parse tree recursively with a scope, starting at that scope's root. */ void traverseAtScope(Scope s) { Node n = s.getRootNode(); if (n.isFunction()) { // We need to do some extra magic to make sure that the scope doesn't // get re-created when we dive into the function. if (inputId == null) { inputId = NodeUtil.getInputId(n); } sourceName = getSourceName(n); curNode = n; pushScope(s); Node args = n.getFirstChild().getNext(); Node body = args.getNext(); traverseBranch(args, n); traverseBranch(body, n); popScope(); } else if (n.isBlock()) { if (inputId == null) { inputId = NodeUtil.getInputId(n); } sourceName = getSourceName(n); curNode = n; pushScope(s); traverseBranch(n, n.getParent()); popScope(); } else { Preconditions.checkState(s.isGlobal(), "Expected global scope. Got:", s); traverseWithScope(n, s); } } /** * Traverse a function out-of-band of normal traversal. * * @param node The function node. * @param scope The scope the function is contained in. Does not fire enter/exit callback events * for this scope. */ public void traverseFunctionOutOfBand(Node node, Scope scope) { Preconditions.checkNotNull(scope); Preconditions.checkState(node.isFunction()); Preconditions.checkState(scope.getRootNode() != null); if (inputId == null) { inputId = NodeUtil.getInputId(node); } curNode = node.getParent(); pushScope(scope, true /* quietly */); traverseBranch(node, curNode); popScope(true /* quietly */); } /** * Traverses an inner node recursively with a refined scope. An inner node may be any node with a * non {@code null} parent (i.e. all nodes except the root). * * @param node the node to traverse * @param parent the node's parent, it may not be {@code null} * @param refinedScope the refined scope of the scope currently at the top of the scope stack or * in trivial cases that very scope or {@code null} */ protected void traverseInnerNode(Node node, Node parent, Scope refinedScope) { Preconditions.checkNotNull(parent); if (inputId == null) { inputId = NodeUtil.getInputId(node); } if (refinedScope != null && getScope() != refinedScope) { curNode = node; pushScope(refinedScope); traverseBranch(node, parent); popScope(); } else { traverseBranch(node, parent); } } public AbstractCompiler getCompiler() { return compiler; } /** * Gets the current line number, or zero if it cannot be determined. The line number is retrieved * lazily as a running time optimization. */ public int getLineNumber() { Node cur = curNode; while (cur != null) { int line = cur.getLineno(); if (line >= 0) { return line; } cur = cur.getParent(); } return 0; } /** * Gets the current char number, or zero if it cannot be determined. The line number is retrieved * lazily as a running time optimization. */ public int getCharno() { Node cur = curNode; while (cur != null) { int line = cur.getCharno(); if (line >= 0) { return line; } cur = cur.getParent(); } return 0; } /** * Gets the current input source name. * * @return A string that may be empty, but not null */ public String getSourceName() { return sourceName; } /** Gets the current input source. */ public CompilerInput getInput() { return compiler.getInput(inputId); } /** Gets the current input module. */ public JSModule getModule() { CompilerInput input = getInput(); return input == null ? null : input.getModule(); } /** Returns the node currently being traversed. */ public Node getCurrentNode() { return curNode; } /** * Traversal for passes that work only on changed functions. Suppose a loopable pass P1 uses this * traversal. Then, if a function doesn't change between two runs of P1, it won't look at the * function the second time. (We're assuming that P1 runs to a fixpoint, o/w we may miss * optimizations.) * * <p>Most changes are reported with calls to Compiler.reportCodeChange(), which doesn't know * which scope changed. We keep track of the current scope by calling Compiler.setScope inside * pushScope and popScope. The automatic tracking can be wrong in rare cases when a pass changes * scope w/out causing a call to pushScope or popScope. It's very hard to find the places where * this happens unless a bug is triggered. Passes that do cross-scope modifications call * Compiler.reportChangeToEnclosingScope(Node n). */ public static void traverseChangedFunctions( AbstractCompiler compiler, FunctionCallback callback) { final AbstractCompiler comp = compiler; final FunctionCallback cb = callback; final Node jsRoot = comp.getJsRoot(); NodeTraversal t = new NodeTraversal( comp, new AbstractPreOrderCallback() { @Override public final boolean shouldTraverse(NodeTraversal t, Node n, Node p) { if ((n == jsRoot || n.isFunction()) && comp.hasScopeChanged(n)) { cb.visit(comp, n); } return true; } }); t.traverse(jsRoot); } /** Traverses a node recursively. */ public static void traverse(AbstractCompiler compiler, Node root, Callback cb) { NodeTraversal t = new NodeTraversal(compiler, cb); t.traverse(root); } public static void traverseTyped(AbstractCompiler compiler, Node root, Callback cb) { NodeTraversal t = new NodeTraversal(compiler, cb, SyntacticScopeCreator.makeTyped(compiler)); t.traverse(root); } public static void traverseRoots( AbstractCompiler compiler, Callback cb, Node externs, Node root) { NodeTraversal t = new NodeTraversal(compiler, cb); t.traverseRoots(externs, root); } static void traverseRootsTyped(AbstractCompiler compiler, Callback cb, Node externs, Node root) { NodeTraversal t = new NodeTraversal(compiler, cb, SyntacticScopeCreator.makeTyped(compiler)); t.traverseRoots(externs, root); } /** Traverses a branch. */ private void traverseBranch(Node n, Node parent) { int type = n.getType(); if (type == Token.SCRIPT) { inputId = n.getInputId(); sourceName = getSourceName(n); } curNode = n; if (!callback.shouldTraverse(this, n, parent)) { return; } if (type == Token.FUNCTION) { traverseFunction(n, parent); } else if (useBlockScope && NodeUtil.createsBlockScope(n)) { traverseBlockScope(n); } else { for (Node child = n.getFirstChild(); child != null; ) { // child could be replaced, in which case our child node // would no longer point to the true next Node next = child.getNext(); traverseBranch(child, n); child = next; } } curNode = n; callback.visit(this, n, parent); } /** Traverses a function. */ private void traverseFunction(Node n, Node parent) { Preconditions.checkState(n.getChildCount() == 3); Preconditions.checkState(n.isFunction()); final Node fnName = n.getFirstChild(); boolean isFunctionExpression = (parent != null) && NodeUtil.isFunctionExpression(n); if (!isFunctionExpression) { // Functions declarations are in the scope containing the declaration. traverseBranch(fnName, n); } curNode = n; pushScope(n); if (isFunctionExpression) { // Function expression names are only accessible within the function // scope. traverseBranch(fnName, n); } final Node args = fnName.getNext(); final Node body = args.getNext(); // Args traverseBranch(args, n); // Body // ES6 "arrow" function may not have a block as a body. traverseBranch(body, n); popScope(); } /** Traverses a non-function block. */ private void traverseBlockScope(Node n) { pushScope(n); for (Node child : n.children()) { traverseBranch(child, n); } popScope(); } /** Examines the functions stack for the last instance of a function node. */ public Node getEnclosingFunction() { Node root = getCfgRoot(); return root.isFunction() ? root : null; } /** Creates a new scope (e.g. when entering a function). */ private void pushScope(Node node) { Preconditions.checkState(curNode != null); compiler.setScope(node); scopeRoots.push(node); if (NodeUtil.isValidCfgRoot(node)) { cfgRoots.push(node); cfgs.push(null); } if (scopeCallback != null) { scopeCallback.enterScope(this); } } /** Creates a new scope (e.g. when entering a function). */ private void pushScope(Scope s) { pushScope(s, false); } /** * Creates a new scope (e.g. when entering a function). * * @param quietly Don't fire an enterScope callback. */ private void pushScope(Scope s, boolean quietly) { Preconditions.checkState(curNode != null); compiler.setScope(s.getRootNode()); scopes.push(s); if (NodeUtil.isValidCfgRoot(s.getRootNode())) { cfgs.push(null); } if (!quietly && scopeCallback != null) { scopeCallback.enterScope(this); } } private void popScope() { popScope(false); } /** * Pops back to the previous scope (e.g. when leaving a function). * * @param quietly Don't fire the exitScope callback. */ private void popScope(boolean quietly) { if (!quietly && scopeCallback != null) { scopeCallback.exitScope(this); } Node scopeRoot; if (scopeRoots.isEmpty()) { scopeRoot = scopes.pop().getRootNode(); } else { scopeRoot = scopeRoots.pop(); } if (NodeUtil.isValidCfgRoot(scopeRoot)) { cfgs.pop(); if (!cfgRoots.isEmpty()) { Preconditions.checkState(cfgRoots.pop() == scopeRoot); } } if (hasScope()) { compiler.setScope(getScopeRoot()); } } /** Gets the current scope. */ public Scope getScope() { Scope scope = scopes.isEmpty() ? null : scopes.peek(); if (scopeRoots.isEmpty()) { return scope; } Iterator<Node> it = scopeRoots.descendingIterator(); while (it.hasNext()) { scope = scopeCreator.createScope(it.next(), scope); scopes.push(scope); } scopeRoots.clear(); cfgRoots.clear(); // No need to call compiler.setScope; the top scopeRoot is now the top scope return scope; } public TypedScope getTypedScope() { Scope s = getScope(); Preconditions.checkState(s instanceof TypedScope, "getTypedScope called for untyped traversal"); return (TypedScope) s; } /** Gets the control flow graph for the current JS scope. */ public ControlFlowGraph<Node> getControlFlowGraph() { if (cfgs.peek() == null) { ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, true); cfa.process(null, getCfgRoot()); cfgs.pop(); cfgs.push(cfa.getCfg()); } return cfgs.peek(); } /** Returns the current scope's root. */ public Node getScopeRoot() { if (scopeRoots.isEmpty()) { return scopes.peek().getRootNode(); } else { return scopeRoots.peek(); } } private Node getCfgRoot() { if (cfgRoots.isEmpty()) { Scope currScope = scopes.peek(); while (currScope.isBlockScope()) { currScope = currScope.getParent(); } return currScope.getRootNode(); } else { return cfgRoots.peek(); } } /** Determines whether the traversal is currently in the global scope. */ boolean inGlobalScope() { return getScopeDepth() <= 1; } // Not dual of inGlobalScope, because of block scoping. // They both return false in an inner block at top level. boolean inFunction() { return getCfgRoot().isFunction(); } int getScopeDepth() { return scopes.size() + scopeRoots.size(); } public boolean hasScope() { return !(scopes.isEmpty() && scopeRoots.isEmpty()); } /** Reports a diagnostic (error or warning) */ public void report(Node n, DiagnosticType diagnosticType, String... arguments) { JSError error = JSError.make(n, diagnosticType, arguments); compiler.report(error); } private static String getSourceName(Node n) { String name = n.getSourceFileName(); return name == null ? "" : name; } InputId getInputId() { return inputId; } /** * Creates a JSError during NodeTraversal. * * @param n Determines the line and char position within the source file name * @param type The DiagnosticType * @param arguments Arguments to be incorporated into the message */ public JSError makeError(Node n, CheckLevel level, DiagnosticType type, String... arguments) { return JSError.make(n, level, type, arguments); } /** * Creates a JSError during NodeTraversal. * * @param n Determines the line and char position within the source file name * @param type The DiagnosticType * @param arguments Arguments to be incorporated into the message */ public JSError makeError(Node n, DiagnosticType type, String... arguments) { return JSError.make(n, type, arguments); } private String getBestSourceFileName(Node n) { return n == null ? sourceName : n.getSourceFileName(); } }
/** * Converts ES6 code to valid ES5 code. This class does most of the transpilation, and * https://github.com/google/closure-compiler/wiki/ECMAScript6 lists which ES6 features are * supported. Other classes that start with "Es6" do other parts of the transpilation. * * <p>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 Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapCompilerPass { private final AbstractCompiler compiler; static final DiagnosticType CANNOT_CONVERT = DiagnosticType.error("JSC_CANNOT_CONVERT", "This code cannot be converted from ES6. {0}"); // TODO(tbreisacher): Remove this once we have implemented transpilation for all the features // we intend to support. static final DiagnosticType CANNOT_CONVERT_YET = DiagnosticType.error( "JSC_CANNOT_CONVERT_YET", "ES6 transpilation of ''{0}'' is not yet implemented."); static final DiagnosticType DYNAMIC_EXTENDS_TYPE = DiagnosticType.error( "JSC_DYNAMIC_EXTENDS_TYPE", "The class in an extends clause must be a qualified name."); static final DiagnosticType CLASS_REASSIGNMENT = DiagnosticType.error( "CLASS_REASSIGNMENT", "Class names defined inside a function cannot be reassigned."); static final DiagnosticType CONFLICTING_GETTER_SETTER_TYPE = DiagnosticType.error( "CONFLICTING_GETTER_SETTER_TYPE", "The types of the getter and setter for property ''{0}'' do not match."); 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"; private static final String FRESH_COMP_PROP_VAR = "$jscomp$compprop"; private static final String ITER_BASE = "$jscomp$iter$"; private static final String ITER_RESULT = "$jscomp$key$"; // These functions are defined in js/es6_runtime.js static final String INHERITS = "$jscomp.inherits"; static final String MAKE_ITER = "$jscomp.makeIterator"; public Es6ToEs3Converter(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, externs, this); NodeTraversal.traverseEs6(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverseEs6(compiler, scriptRoot, 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.getType()) { case Token.REST: visitRestParam(n, parent); break; case Token.GETTER_DEF: case Token.SETTER_DEF: if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) { cannotConvert(n, "ES5 getters/setters (consider using --language_out=ES5)"); return false; } break; } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.OBJECTLIT: for (Node child : n.children()) { if (child.isComputedProp()) { visitObjectWithComputedProperty(n, parent); break; } } break; case Token.MEMBER_FUNCTION_DEF: if (parent.isObjectLit()) { visitMemberDefInObjectLit(n, parent); } break; case Token.FOR_OF: visitForOf(n, parent); break; case Token.STRING_KEY: visitStringKey(n); break; case Token.CLASS: for (Node member = n.getLastChild().getFirstChild(); member != null; member = member.getNext()) { if (member.getBooleanProp(Node.COMPUTED_PROP_GETTER) || member.getBooleanProp(Node.COMPUTED_PROP_SETTER)) { cannotConvert(member, "computed getter or setter in class definition"); return; } } visitClass(n, parent); break; case Token.ARRAYLIT: case Token.NEW: case Token.CALL: for (Node child : n.children()) { if (child.isSpread()) { visitArrayLitOrCallWithSpread(n, parent); break; } } break; case Token.TAGGED_TEMPLATELIT: Es6TemplateLiterals.visitTaggedTemplateLiteral(t, n); break; case Token.TEMPLATELIT: if (!parent.isTaggedTemplateLit()) { Es6TemplateLiterals.visitTemplateLiteral(t, n); } break; } } /** * Converts a member definition in an object literal to an ES3 key/value pair. Member definitions * in classes are handled in {@link #visitClass}. */ private void visitMemberDefInObjectLit(Node n, Node parent) { String name = n.getString(); Node stringKey = IR.stringKey(name, n.getFirstChild().detachFromParent()); parent.replaceChild(n, stringKey); compiler.reportCodeChange(); } /** Converts extended object literal {a} to {a:a}. */ private void visitStringKey(Node n) { if (!n.hasChildren()) { Node name = IR.name(n.getString()); name.useSourceInfoIfMissingFrom(n); n.addChildToBack(name); compiler.reportCodeChange(); } } private void visitForOf(Node node, Node parent) { Node variable = node.removeFirstChild(); Node iterable = node.removeFirstChild(); Node body = node.removeFirstChild(); Node iterName = IR.name(ITER_BASE + compiler.getUniqueNameIdSupplier().get()); Node getNext = IR.call(IR.getprop(iterName.cloneTree(), IR.string("next"))); String variableName; int declType; if (variable.isName()) { declType = Token.NAME; variableName = variable.getQualifiedName(); } else { Preconditions.checkState( NodeUtil.isNameDeclaration(variable), "Expected var, let, or const. Got %s", variable); declType = variable.getType(); variableName = variable.getFirstChild().getQualifiedName(); } Node iterResult = IR.name(ITER_RESULT + variableName); Node makeIter = IR.call(NodeUtil.newQName(compiler, MAKE_ITER), iterable); compiler.needsEs6Runtime = true; Node init = IR.var(iterName.cloneTree(), makeIter); Node initIterResult = iterResult.cloneTree(); initIterResult.addChildToFront(getNext.cloneTree()); init.addChildToBack(initIterResult); Node cond = IR.not(IR.getprop(iterResult.cloneTree(), IR.string("done"))); Node incr = IR.assign(iterResult.cloneTree(), getNext.cloneTree()); Node declarationOrAssign; if (declType == Token.NAME) { declarationOrAssign = IR.exprResult( IR.assign( IR.name(variableName), IR.getprop(iterResult.cloneTree(), IR.string("value")))); } else { declarationOrAssign = new Node(declType, IR.name(variableName)); declarationOrAssign .getFirstChild() .addChildToBack(IR.getprop(iterResult.cloneTree(), IR.string("value"))); } body.addChildToFront(declarationOrAssign); Node newFor = IR.forNode(init, cond, incr, body); newFor.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, newFor); compiler.reportCodeChange(); } private void checkClassReassignment(Node clazz) { Node name = NodeUtil.getClassNameNode(clazz); Node enclosingFunction = NodeUtil.getEnclosingFunction(clazz); if (enclosingFunction == null) { return; } CheckClassAssignments checkAssigns = new CheckClassAssignments(name); NodeTraversal.traverseEs6(compiler, enclosingFunction, checkAssigns); } /** Processes a rest parameter */ private void visitRestParam(Node restParam, Node paramList) { Node functionBody = paramList.getLastSibling(); restParam.setType(Token.NAME); restParam.setVarArgs(true); // Make sure rest parameters are typechecked JSTypeExpression type = null; JSDocInfo info = restParam.getJSDocInfo(); String paramName = restParam.getString(); if (info != null) { type = info.getType(); } else { JSDocInfo functionInfo = paramList.getParent().getJSDocInfo(); if (functionInfo != null) { type = functionInfo.getParameterType(paramName); } } if (type != null && type.getRoot().getType() != Token.ELLIPSIS) { compiler.report(JSError.make(restParam, BAD_REST_PARAMETER_ANNOTATION)); } if (!functionBody.hasChildren()) { // If function has no body, we are done! compiler.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.detachFromParent()); } if (type != null) { Node arrayType = IR.string("Array"); Node typeNode = type.getRoot(); Node memberType = typeNode.getType() == Token.ELLIPSIS ? typeNode.getFirstChild().cloneNode() : typeNode.cloneNode(); 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()); } int restIndex = paramList.getIndexOfChild(restParam); 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.reportCodeChange(); // 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. } /** * Processes array literals or calls containing spreads. Eg.: [1, 2, ...x, 4, 5] => [1, * 2].concat(x, [4, 5]); Eg.: f(...arr) => f.apply(null, arr) Eg.: new F(...args) => new * Function.prototype.bind.apply(F, [].concat(args)) */ private void visitArrayLitOrCallWithSpread(Node node, Node parent) { Preconditions.checkArgument(node.isCall() || node.isArrayLit() || node.isNew()); List<Node> 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(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 joinedGroups = IR.call( IR.getprop(IR.arraylit(), IR.string("concat")), groups.toArray(new Node[groups.size()])); 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 { Node bindApply = NodeUtil.newQName(compiler, "Function.prototype.bind.apply"); result = IR.newNode(bindApply, callee, joinedGroups); } result.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, result); compiler.reportCodeChange(); } private void visitObjectWithComputedProperty(Node obj, Node parent) { Preconditions.checkArgument(obj.isObjectLit()); List<Node> props = new ArrayList<>(); Node currElement = obj.getFirstChild(); while (currElement != null) { if (currElement.getBooleanProp(Node.COMPUTED_PROP_GETTER) || currElement.getBooleanProp(Node.COMPUTED_PROP_SETTER)) { cannotConvertYet(currElement, "computed getter/setter"); return; } else if (currElement.isGetterDef() || currElement.isSetterDef()) { currElement = currElement.getNext(); } else { Node nextNode = currElement.getNext(); obj.removeChild(currElement); props.add(currElement); currElement = nextNode; } } String objName = FRESH_COMP_PROP_VAR + compiler.getUniqueNameIdSupplier().get(); props = Lists.reverse(props); Node result = IR.name(objName); for (Node propdef : props) { if (propdef.isComputedProp()) { Node propertyExpression = propdef.removeFirstChild(); Node value = propdef.removeFirstChild(); result = IR.comma(IR.assign(IR.getelem(IR.name(objName), propertyExpression), value), result); } else { if (!propdef.hasChildren()) { Node name = IR.name(propdef.getString()).useSourceInfoIfMissingFrom(propdef); propdef.addChildToBack(name); } Node val = propdef.removeFirstChild(); propdef.setType(Token.STRING); int type = propdef.isQuotedString() ? Token.GETELEM : Token.GETPROP; Node access = new Node(type, IR.name(objName), propdef); result = IR.comma(IR.assign(access, val), result); } } Node statement = obj; while (!NodeUtil.isStatement(statement)) { statement = statement.getParent(); } result.useSourceInfoIfMissingFromForTree(obj); parent.replaceChild(obj, result); Node var = IR.var(IR.name(objName), obj); var.useSourceInfoIfMissingFromForTree(statement); statement.getParent().addChildBefore(var, statement); compiler.reportCodeChange(); } /** * Classes are processed in 3 phases: * * <ol> * <li>The class name is extracted. * <li>Class members are processed and rewritten. * <li>The constructor is built. * </ol> */ private void visitClass(Node classNode, Node parent) { checkClassReassignment(classNode); // Collect Metadata ClassDeclarationMetadata metadata = ClassDeclarationMetadata.create(classNode, parent); if (metadata == null || metadata.fullClassName == null) { cannotConvert( parent, "Can only convert classes that are declarations or the right hand" + " side of a simple assignment."); return; } if (metadata.hasSuperClass() && !metadata.superClassNameNode.isQualifiedName()) { compiler.report(JSError.make(metadata.superClassNameNode, DYNAMIC_EXTENDS_TYPE)); return; } boolean useUnique = NodeUtil.isStatement(classNode) && !NodeUtil.isInFunction(classNode); String uniqueFullClassName = useUnique ? getUniqueClassName(metadata.fullClassName) : metadata.fullClassName; Node classNameAccess = NodeUtil.newQName(compiler, uniqueFullClassName); Node prototypeAccess = NodeUtil.newPropertyAccess(compiler, classNameAccess, "prototype"); Preconditions.checkState( NodeUtil.isStatement(metadata.insertionPoint), "insertion point must be a statement: %s", metadata.insertionPoint); Node constructor = null; JSDocInfo ctorJSDocInfo = null; // Process all members of the class Node classMembers = classNode.getLastChild(); Map<String, JSDocInfo> prototypeMembersToDeclare = new LinkedHashMap<>(); Map<String, JSDocInfo> classMembersToDeclare = new LinkedHashMap<>(); for (Node member : classMembers.children()) { if (member.isEmpty()) { continue; } Preconditions.checkState( member.isMemberFunctionDef() || member.isGetterDef() || member.isSetterDef() || (member.isComputedProp() && !member.getBooleanProp(Node.COMPUTED_PROP_VARIABLE)), "Member variables should have been transpiled earlier: ", member); if (member.isGetterDef() || member.isSetterDef()) { JSTypeExpression typeExpr = getTypeFromGetterOrSetter(member).clone(); addToDefinePropertiesObject(metadata, member); Map<String, JSDocInfo> membersToDeclare = member.isStaticMember() ? classMembersToDeclare : prototypeMembersToDeclare; JSDocInfo existingJSDoc = membersToDeclare.get(member.getString()); JSTypeExpression existingType = existingJSDoc == null ? null : existingJSDoc.getType(); if (existingType != null && !existingType.equals(typeExpr)) { compiler.report(JSError.make(member, CONFLICTING_GETTER_SETTER_TYPE, member.getString())); } else { JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false); jsDoc.recordType(typeExpr); if (member.getJSDocInfo() != null && member.getJSDocInfo().isExport()) { jsDoc.recordExport(); } if (member.isStaticMember()) { jsDoc.recordNoCollapse(); } membersToDeclare.put(member.getString(), jsDoc.build()); } } else if (member.isMemberFunctionDef() && member.getString().equals("constructor")) { ctorJSDocInfo = member.getJSDocInfo(); constructor = member.getFirstChild().detachFromParent(); if (!metadata.anonymous) { // Turns class Foo { constructor: function() {} } into function Foo() {}, // i.e. attaches the name the ctor function. constructor.replaceChild(constructor.getFirstChild(), metadata.classNameNode.cloneNode()); } } else { Node qualifiedMemberAccess = getQualifiedMemberAccess(compiler, member, classNameAccess, prototypeAccess); Node method = member.getLastChild().detachFromParent(); Node assign = IR.assign(qualifiedMemberAccess, method); assign.useSourceInfoIfMissingFromForTree(member); JSDocInfo info = member.getJSDocInfo(); if (member.isStaticMember() && NodeUtil.referencesThis(assign.getLastChild())) { JSDocInfoBuilder memberDoc = JSDocInfoBuilder.maybeCopyFrom(info); memberDoc.recordThisType( new JSTypeExpression( new Node(Token.BANG, new Node(Token.QMARK)), member.getSourceFileName())); info = memberDoc.build(); } if (info != null) { assign.setJSDocInfo(info); } Node newNode = NodeUtil.newExpr(assign); metadata.insertNodeAndAdvance(newNode); } } // Add declarations for properties that were defined with a getter and/or setter, // so that the typechecker knows those properties exist on the class. // This is a temporary solution. Eventually, the type checker should understand // Object.defineProperties calls directly. for (Map.Entry<String, JSDocInfo> entry : prototypeMembersToDeclare.entrySet()) { String declaredMember = entry.getKey(); Node declaration = IR.getprop(prototypeAccess.cloneTree(), IR.string(declaredMember)); declaration.setJSDocInfo(entry.getValue()); metadata.insertNodeAndAdvance( IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(classNode)); } for (Map.Entry<String, JSDocInfo> entry : classMembersToDeclare.entrySet()) { String declaredMember = entry.getKey(); Node declaration = IR.getprop(classNameAccess.cloneTree(), IR.string(declaredMember)); declaration.setJSDocInfo(entry.getValue()); metadata.insertNodeAndAdvance( IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(classNode)); } if (metadata.definePropertiesObjForPrototype.hasChildren()) { Node definePropsCall = IR.exprResult( IR.call( NodeUtil.newQName(compiler, "Object.defineProperties"), prototypeAccess.cloneTree(), metadata.definePropertiesObjForPrototype)); definePropsCall.useSourceInfoIfMissingFromForTree(classNode); metadata.insertNodeAndAdvance(definePropsCall); } if (metadata.definePropertiesObjForClass.hasChildren()) { Node definePropsCall = IR.exprResult( IR.call( NodeUtil.newQName(compiler, "Object.defineProperties"), classNameAccess.cloneTree(), metadata.definePropertiesObjForClass)); definePropsCall.useSourceInfoIfMissingFromForTree(classNode); metadata.insertNodeAndAdvance(definePropsCall); } Preconditions.checkNotNull(constructor); JSDocInfo classJSDoc = NodeUtil.getBestJSDocInfo(classNode); JSDocInfoBuilder newInfo = JSDocInfoBuilder.maybeCopyFrom(classJSDoc); newInfo.recordConstructor(); if (metadata.hasSuperClass()) { String superClassString = metadata.superClassNameNode.getQualifiedName(); if (newInfo.isInterfaceRecorded()) { newInfo.recordExtendedInterface( new JSTypeExpression( new Node(Token.BANG, IR.string(superClassString)), metadata.superClassNameNode.getSourceFileName())); } else { Node inherits = IR.call( NodeUtil.newQName(compiler, INHERITS), NodeUtil.newQName(compiler, metadata.fullClassName), NodeUtil.newQName(compiler, superClassString)); Node inheritsCall = IR.exprResult(inherits); compiler.needsEs6Runtime = true; inheritsCall.useSourceInfoIfMissingFromForTree(classNode); Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode); enclosingStatement.getParent().addChildAfter(inheritsCall, enclosingStatement); newInfo.recordBaseType( new JSTypeExpression( new Node(Token.BANG, IR.string(superClassString)), metadata.superClassNameNode.getSourceFileName())); } } // Classes are @struct by default. if (!newInfo.isUnrestrictedRecorded() && !newInfo.isDictRecorded() && !newInfo.isStructRecorded()) { newInfo.recordStruct(); } if (ctorJSDocInfo != null) { newInfo.recordSuppressions(ctorJSDocInfo.getSuppressions()); for (String param : ctorJSDocInfo.getParameterNames()) { newInfo.recordParameter(param, ctorJSDocInfo.getParameterType(param)); } newInfo.mergePropertyBitfieldFrom(ctorJSDocInfo); } if (NodeUtil.isStatement(classNode)) { constructor.getFirstChild().setString(""); Node ctorVar = IR.let(metadata.classNameNode.cloneNode(), constructor); ctorVar.useSourceInfoIfMissingFromForTree(classNode); parent.replaceChild(classNode, ctorVar); } else { parent.replaceChild(classNode, constructor); } if (NodeUtil.isStatement(constructor)) { constructor.setJSDocInfo(newInfo.build()); } else if (parent.isName()) { // The constructor function is the RHS of a var statement. // Add the JSDoc to the VAR node. Node var = parent.getParent(); var.setJSDocInfo(newInfo.build()); } else if (constructor.getParent().isName()) { // Is a newly created VAR node. Node var = constructor.getParent().getParent(); var.setJSDocInfo(newInfo.build()); } else if (parent.isAssign()) { // The constructor function is the RHS of an assignment. // Add the JSDoc to the ASSIGN node. parent.setJSDocInfo(newInfo.build()); } else { throw new IllegalStateException("Unexpected parent node " + parent); } compiler.reportCodeChange(); } /** @param node A getter or setter node. */ private JSTypeExpression getTypeFromGetterOrSetter(Node node) { JSDocInfo info = node.getJSDocInfo(); if (info != null) { if (node.isGetterDef() && info.getReturnType() != null) { return info.getReturnType(); } else { Set<String> paramNames = info.getParameterNames(); if (paramNames.size() == 1) { return info.getParameterType(Iterables.getOnlyElement(info.getParameterNames())); } } } return new JSTypeExpression(new Node(Token.QMARK), node.getSourceFileName()); } private void addToDefinePropertiesObject(ClassDeclarationMetadata metadata, Node member) { Node obj = member.isStaticMember() ? metadata.definePropertiesObjForClass : metadata.definePropertiesObjForPrototype; Node prop = NodeUtil.getFirstPropMatchingKey(obj, member.getString()); if (prop == null) { prop = IR.objectlit( IR.stringKey("configurable", IR.trueNode()), IR.stringKey("enumerable", IR.trueNode())); obj.addChildToBack(IR.stringKey(member.getString(), prop)); } Node function = member.getLastChild(); JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(NodeUtil.getBestJSDocInfo(function)); info.recordThisType( new JSTypeExpression( new Node(Token.BANG, IR.string(metadata.fullClassName)), member.getSourceFileName())); Node stringKey = IR.stringKey(member.isGetterDef() ? "get" : "set", function.detachFromParent()); stringKey.setJSDocInfo(info.build()); prop.addChildToBack(stringKey); prop.useSourceInfoIfMissingFromForTree(member); } /** * Constructs a Node that represents an access to the given class member, qualified by either the * static or the instance access context, depending on whether the member is static. * * <p><b>WARNING:</b> {@code member} may be modified/destroyed by this method, do not use it * afterwards. */ static Node getQualifiedMemberAccess( AbstractCompiler compiler, Node member, Node staticAccess, Node instanceAccess) { Node context = member.isStaticMember() ? staticAccess : instanceAccess; context = context.cloneTree(); if (member.isComputedProp()) { return IR.getelem(context, member.removeFirstChild()); } else { return NodeUtil.newPropertyAccess(compiler, context, member.getString()); } } private static String getUniqueClassName(String qualifiedName) { return qualifiedName; } private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback { private Node className; public CheckClassAssignments(Node className) { this.className = className; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.isAssign() || n.getFirstChild() == className) { return; } if (className.matchesQualifiedName(n.getFirstChild())) { compiler.report(JSError.make(n, CLASS_REASSIGNMENT)); } } } private void cannotConvert(Node n, String message) { compiler.report(JSError.make(n, CANNOT_CONVERT, message)); } /** * Warns the user that the given ES6 feature cannot be converted to ES3 because the transpilation * is not yet implemented. A call to this method is essentially a "TODO(tbreisacher): Implement * {@code feature}" comment. */ private void cannotConvertYet(Node n, String feature) { compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature)); } /** * Represents static metadata on a class declaration expression - i.e. the qualified name that a * class declares (directly or by assignment), whether it's anonymous, and where transpiled code * should be inserted (i.e. which object will hold the prototype after transpilation). */ static class ClassDeclarationMetadata { /** A statement node. Transpiled methods etc of the class are inserted after this node. */ private Node insertionPoint; /** * An object literal node that will be used in a call to Object.defineProperties, to add getters * and setters to the prototype. */ private final Node definePropertiesObjForPrototype; /** * An object literal node that will be used in a call to Object.defineProperties, to add getters * and setters to the class. */ private final Node definePropertiesObjForClass; /** * The fully qualified name of the class, which will be used in the output. May come from the * class itself or the LHS of an assignment. */ final String fullClassName; /** Whether the constructor function in the output should be anonymous. */ final boolean anonymous; final Node classNameNode; final Node superClassNameNode; private ClassDeclarationMetadata( Node insertionPoint, String fullClassName, boolean anonymous, Node classNameNode, Node superClassNameNode) { this.insertionPoint = insertionPoint; this.definePropertiesObjForClass = IR.objectlit(); this.definePropertiesObjForPrototype = IR.objectlit(); this.fullClassName = fullClassName; this.anonymous = anonymous; this.classNameNode = classNameNode; this.superClassNameNode = superClassNameNode; } static ClassDeclarationMetadata create(Node classNode, Node parent) { Node classNameNode = classNode.getFirstChild(); Node superClassNameNode = classNameNode.getNext(); // If this is a class statement, or a class expression in a simple // assignment or var statement, convert it. In any other case, the // code is too dynamic, so return null. if (NodeUtil.isStatement(classNode)) { return new ClassDeclarationMetadata( classNode, classNameNode.getString(), false, classNameNode, superClassNameNode); } else if (parent.isAssign() && parent.getParent().isExprResult()) { // Add members after the EXPR_RESULT node: // example.C = class {}; example.C.prototype.foo = function() {}; String fullClassName = parent.getFirstChild().getQualifiedName(); if (fullClassName == null) { return null; } return new ClassDeclarationMetadata( parent.getParent(), fullClassName, true, classNameNode, superClassNameNode); } else if (parent.isName()) { // Add members after the 'var' statement. // var C = class {}; C.prototype.foo = function() {}; return new ClassDeclarationMetadata( parent.getParent(), parent.getString(), true, classNameNode, superClassNameNode); } else { // Cannot handle this class declaration. return null; } } void insertNodeAndAdvance(Node newNode) { insertionPoint.getParent().addChildAfter(newNode, insertionPoint); insertionPoint = newNode; } boolean hasSuperClass() { return !superClassNameNode.isEmpty(); } } }
/** * A builder for FunctionTypes, because FunctionTypes are so ridiculously complex. All methods * return {@code this} for ease of use. * * <p>Right now, this mostly uses JSDocInfo to infer type information about functions. In the long * term, developers should extend it to use other signals by overloading the various "inferXXX" * methods. For example, we might want to use {@code goog.inherits} calls as a signal for * inheritance, or {@code return} statements as a signal for return type. * * <p>NOTE(nicksantos): Organizationally, this feels like it should be in Rhino. But it depends on * some coding convention stuff that's really part of JSCompiler. * * @author [email protected] (Nick Santos) * @author [email protected] (Pascal-Louis Perez) */ final class FunctionTypeBuilder { private final String fnName; private final AbstractCompiler compiler; private final CodingConvention codingConvention; private final JSTypeRegistry typeRegistry; private final Node errorRoot; private final String sourceName; private final Scope scope; private JSType returnType = null; private boolean returnTypeInferred = false; private List<ObjectType> implementedInterfaces = null; private ObjectType baseType = null; private ObjectType thisType = null; private boolean isConstructor = false; private boolean isInterface = false; private Node parametersNode = null; private Node sourceNode = null; private String templateTypeName = null; static final DiagnosticType EXTENDS_WITHOUT_TYPEDEF = DiagnosticType.warning( "JSC_EXTENDS_WITHOUT_TYPEDEF", "@extends used without @constructor or @interface for {0}"); static final DiagnosticType EXTENDS_NON_OBJECT = DiagnosticType.warning("JSC_EXTENDS_NON_OBJECT", "{0} @extends non-object type {1}"); static final DiagnosticType RESOLVED_TAG_EMPTY = DiagnosticType.warning("JSC_RESOLVED_TAG_EMPTY", "Could not resolve type in {0} tag of {1}"); static final DiagnosticType IMPLEMENTS_WITHOUT_CONSTRUCTOR = DiagnosticType.warning( "JSC_IMPLEMENTS_WITHOUT_CONSTRUCTOR", "@implements used without @constructor or @interface for {0}"); static final DiagnosticType VAR_ARGS_MUST_BE_LAST = DiagnosticType.warning("JSC_VAR_ARGS_MUST_BE_LAST", "variable length argument must be last"); static final DiagnosticType OPTIONAL_ARG_AT_END = DiagnosticType.warning("JSC_OPTIONAL_ARG_AT_END", "optional arguments must be at the end"); static final DiagnosticType INEXISTANT_PARAM = DiagnosticType.warning( "JSC_INEXISTANT_PARAM", "parameter {0} does not appear in {1}''s parameter list"); static final DiagnosticType TYPE_REDEFINITION = DiagnosticType.warning( "JSC_TYPE_REDEFINITION", "attempted re-definition of type {0}\n" + "found : {1}\n" + "expected: {2}"); static final DiagnosticType TEMPLATE_TYPE_DUPLICATED = DiagnosticType.error( "JSC_TEMPLATE_TYPE_DUPLICATED", "Only one parameter type must be the template type"); static final DiagnosticType TEMPLATE_TYPE_EXPECTED = DiagnosticType.error( "JSC_TEMPLATE_TYPE_EXPECTED", "The template type must be a parameter type"); static final DiagnosticType THIS_TYPE_NON_OBJECT = DiagnosticType.warning( "JSC_THIS_TYPE_NON_OBJECT", "@this type of a function must be an object\n" + "Actual type: {0}"); private class ExtendedTypeValidator implements Predicate<JSType> { @Override public boolean apply(JSType type) { ObjectType objectType = ObjectType.cast(type); if (objectType == null) { reportWarning(EXTENDS_NON_OBJECT, fnName, type.toString()); } else if (objectType.isUnknownType() && // If this has a supertype that hasn't been resolved yet, // then we can assume this type will be ok once the super // type resolves. (objectType.getImplicitPrototype() == null || objectType.getImplicitPrototype().isResolved())) { reportWarning(RESOLVED_TAG_EMPTY, "@extends", fnName); } else { return true; } return false; } } private class ImplementedTypeValidator implements Predicate<JSType> { @Override public boolean apply(JSType type) { ObjectType objectType = ObjectType.cast(type); if (objectType == null) { reportError(BAD_IMPLEMENTED_TYPE, fnName); } else if (objectType.isUnknownType() && // If this has a supertype that hasn't been resolved yet, // then we can assume this type will be ok once the super // type resolves. (objectType.getImplicitPrototype() == null || objectType.getImplicitPrototype().isResolved())) { reportWarning(RESOLVED_TAG_EMPTY, "@implements", fnName); } else { return true; } return false; } } private class ThisTypeValidator implements Predicate<JSType> { @Override public boolean apply(JSType type) { // TODO(user): Doing an instanceof check here is too // restrictive as (Date,Error) is, for instance, an object type // even though its implementation is a UnionType. Would need to // create interfaces JSType, ObjectType, FunctionType etc and have // separate implementation instead of the class hierarchy, so that // union types can also be object types, etc. if (!type.restrictByNotNullOrUndefined().isSubtype(typeRegistry.getNativeType(OBJECT_TYPE))) { reportWarning(THIS_TYPE_NON_OBJECT, type.toString()); return false; } return true; } } /** * @param fnName The function name. * @param compiler The compiler. * @param errorRoot The node to associate with any warning generated by this builder. * @param sourceName A source name for associating any warnings that we have to emit. * @param scope The syntactic scope. */ FunctionTypeBuilder( String fnName, AbstractCompiler compiler, Node errorRoot, String sourceName, Scope scope) { Preconditions.checkNotNull(errorRoot); this.fnName = fnName == null ? "" : fnName; this.codingConvention = compiler.getCodingConvention(); this.typeRegistry = compiler.getTypeRegistry(); this.errorRoot = errorRoot; this.sourceName = sourceName; this.compiler = compiler; this.scope = scope; } /** Sets the FUNCTION node of this function. */ FunctionTypeBuilder setSourceNode(@Nullable Node sourceNode) { this.sourceNode = sourceNode; return this; } /** * Infer the parameter and return types of a function from the parameter and return types of the * function it is overriding. * * @param oldType The function being overridden. Does nothing if this is null. * @param paramsParent The LP node of the function that we're assigning to. If null, that just * means we're not initializing this to a function literal. */ FunctionTypeBuilder inferFromOverriddenFunction( @Nullable FunctionType oldType, @Nullable Node paramsParent) { if (oldType == null) { return this; } returnType = oldType.getReturnType(); returnTypeInferred = oldType.isReturnTypeInferred(); if (paramsParent == null) { // Not a function literal. parametersNode = oldType.getParametersNode(); if (parametersNode == null) { parametersNode = new FunctionParamBuilder(typeRegistry).build(); } } else { // We're overriding with a function literal. Apply type information // to each parameter of the literal. FunctionParamBuilder paramBuilder = new FunctionParamBuilder(typeRegistry); Iterator<Node> oldParams = oldType.getParameters().iterator(); boolean warnedAboutArgList = false; boolean oldParamsListHitOptArgs = false; for (Node currentParam = paramsParent.getFirstChild(); currentParam != null; currentParam = currentParam.getNext()) { if (oldParams.hasNext()) { Node oldParam = oldParams.next(); Node newParam = paramBuilder.newParameterFromNode(oldParam); oldParamsListHitOptArgs = oldParamsListHitOptArgs || oldParam.isVarArgs() || oldParam.isOptionalArg(); // The subclass method might right its var_args as individual // arguments. if (currentParam.getNext() != null && newParam.isVarArgs()) { newParam.setVarArgs(false); newParam.setOptionalArg(true); } } else { warnedAboutArgList |= addParameter( paramBuilder, typeRegistry.getNativeType(UNKNOWN_TYPE), warnedAboutArgList, codingConvention.isOptionalParameter(currentParam) || oldParamsListHitOptArgs, codingConvention.isVarArgsParameter(currentParam)); } } parametersNode = paramBuilder.build(); } return this; } /** Infer the return type from JSDocInfo. */ FunctionTypeBuilder inferReturnType(@Nullable JSDocInfo info) { if (info != null && info.hasReturnType()) { returnType = info.getReturnType().evaluate(scope, typeRegistry); returnTypeInferred = false; } if (templateTypeName != null && returnType != null && returnType.restrictByNotNullOrUndefined().isTemplateType()) { reportError(TEMPLATE_TYPE_EXPECTED, fnName); } return this; } /** * If we haven't found a return value yet, try to look at the "return" statements in the function. */ FunctionTypeBuilder inferReturnStatementsAsLastResort(@Nullable Node functionBlock) { if (functionBlock == null || compiler.getInput(sourceName).isExtern()) { return this; } Preconditions.checkArgument(functionBlock.getType() == Token.BLOCK); if (returnType == null) { boolean hasNonEmptyReturns = false; List<Node> worklist = Lists.newArrayList(functionBlock); while (!worklist.isEmpty()) { Node current = worklist.remove(worklist.size() - 1); int cType = current.getType(); if (cType == Token.RETURN && current.getFirstChild() != null || cType == Token.THROW) { hasNonEmptyReturns = true; break; } else if (NodeUtil.isStatementBlock(current) || NodeUtil.isControlStructure(current)) { for (Node child = current.getFirstChild(); child != null; child = child.getNext()) { worklist.add(child); } } } if (!hasNonEmptyReturns) { returnType = typeRegistry.getNativeType(VOID_TYPE); returnTypeInferred = true; } } return this; } /** * Infer the role of the function (whether it's a constructor or interface) and what it inherits * from in JSDocInfo. */ FunctionTypeBuilder inferInheritance(@Nullable JSDocInfo info) { if (info != null) { isConstructor = info.isConstructor(); isInterface = info.isInterface(); // base type if (info.hasBaseType()) { if (isConstructor || isInterface) { JSType maybeBaseType = info.getBaseType().evaluate(scope, typeRegistry); if (maybeBaseType != null && maybeBaseType.setValidator(new ExtendedTypeValidator())) { baseType = (ObjectType) maybeBaseType; } } else { reportWarning(EXTENDS_WITHOUT_TYPEDEF, fnName); } } // implemented interfaces if (isConstructor || isInterface) { implementedInterfaces = Lists.newArrayList(); for (JSTypeExpression t : info.getImplementedInterfaces()) { JSType maybeInterType = t.evaluate(scope, typeRegistry); if (maybeInterType != null && maybeInterType.setValidator(new ImplementedTypeValidator())) { implementedInterfaces.add((ObjectType) maybeInterType); } } if (baseType != null) { JSType maybeFunctionType = baseType.getConstructor(); if (maybeFunctionType instanceof FunctionType) { FunctionType functionType = baseType.getConstructor(); Iterables.addAll(implementedInterfaces, functionType.getImplementedInterfaces()); } } } else if (info.getImplementedInterfaceCount() > 0) { reportWarning(IMPLEMENTS_WITHOUT_CONSTRUCTOR, fnName); } } return this; } /** * Infers the type of {@code this}. * * @param type The type of this. */ FunctionTypeBuilder inferThisType(JSDocInfo info, JSType type) { ObjectType objType = ObjectType.cast(type); if (objType != null && (info == null || !info.hasType())) { thisType = objType; } return this; } /** * Infers the type of {@code this}. * * @param info The JSDocInfo for this function. * @param owner The node for the object whose prototype "owns" this function. For example, {@code * A} in the expression {@code A.prototype.foo}. May be null to indicate that this is not a * prototype property. */ FunctionTypeBuilder inferThisType(JSDocInfo info, @Nullable Node owner) { ObjectType maybeThisType = null; if (info != null && info.hasThisType()) { maybeThisType = ObjectType.cast(info.getThisType().evaluate(scope, typeRegistry)); } if (maybeThisType != null) { thisType = maybeThisType; thisType.setValidator(new ThisTypeValidator()); } else if (owner != null && (info == null || !info.hasType())) { // If the function is of the form: // x.prototype.y = function() {} // then we can assume "x" is the @this type. On the other hand, // if it's of the form: // /** @type {Function} */ x.prototype.y; // then we should not give it a @this type. String ownerTypeName = owner.getQualifiedName(); ObjectType ownerType = ObjectType.cast( typeRegistry.getForgivingType( scope, ownerTypeName, sourceName, owner.getLineno(), owner.getCharno())); if (ownerType != null) { thisType = ownerType; } } return this; } /** Infer the parameter types from the doc info alone. */ FunctionTypeBuilder inferParameterTypes(JSDocInfo info) { // Create a fake args parent. Node lp = new Node(Token.LP); for (String name : info.getParameterNames()) { lp.addChildToBack(Node.newString(Token.NAME, name)); } return inferParameterTypes(lp, info); } /** Infer the parameter types from the list of argument names and the doc info. */ FunctionTypeBuilder inferParameterTypes(@Nullable Node argsParent, @Nullable JSDocInfo info) { if (argsParent == null) { if (info == null) { return this; } else { return inferParameterTypes(info); } } // arguments Node oldParameterType = null; if (parametersNode != null) { oldParameterType = parametersNode.getFirstChild(); } FunctionParamBuilder builder = new FunctionParamBuilder(typeRegistry); boolean warnedAboutArgList = false; Set<String> allJsDocParams = (info == null) ? Sets.<String>newHashSet() : Sets.newHashSet(info.getParameterNames()); boolean foundTemplateType = false; for (Node arg : argsParent.children()) { String argumentName = arg.getString(); allJsDocParams.remove(argumentName); // type from JSDocInfo JSType parameterType = null; boolean isOptionalParam = isOptionalParameter(arg, info); boolean isVarArgs = isVarArgsParameter(arg, info); if (info != null && info.hasParameterType(argumentName)) { parameterType = info.getParameterType(argumentName).evaluate(scope, typeRegistry); } else if (oldParameterType != null && oldParameterType.getJSType() != null) { parameterType = oldParameterType.getJSType(); isOptionalParam = oldParameterType.isOptionalArg(); isVarArgs = oldParameterType.isVarArgs(); } else { parameterType = typeRegistry.getNativeType(UNKNOWN_TYPE); } if (templateTypeName != null && parameterType.restrictByNotNullOrUndefined().isTemplateType()) { if (foundTemplateType) { reportError(TEMPLATE_TYPE_DUPLICATED, fnName); } foundTemplateType = true; } warnedAboutArgList |= addParameter(builder, parameterType, warnedAboutArgList, isOptionalParam, isVarArgs); if (oldParameterType != null) { oldParameterType = oldParameterType.getNext(); } } if (templateTypeName != null && !foundTemplateType) { reportError(TEMPLATE_TYPE_EXPECTED, fnName); } for (String inexistentName : allJsDocParams) { reportWarning(INEXISTANT_PARAM, inexistentName, fnName); } parametersNode = builder.build(); return this; } /** @return Whether the given param is an optional param. */ private boolean isOptionalParameter(Node param, @Nullable JSDocInfo info) { if (codingConvention.isOptionalParameter(param)) { return true; } String paramName = param.getString(); return info != null && info.hasParameterType(paramName) && info.getParameterType(paramName).isOptionalArg(); } /** * Determine whether this is a var args parameter. * * @return Whether the given param is a var args param. */ private boolean isVarArgsParameter(Node param, @Nullable JSDocInfo info) { if (codingConvention.isVarArgsParameter(param)) { return true; } String paramName = param.getString(); return info != null && info.hasParameterType(paramName) && info.getParameterType(paramName).isVarArgs(); } /** Infer the template type from the doc info. */ FunctionTypeBuilder inferTemplateTypeName(@Nullable JSDocInfo info) { if (info != null) { templateTypeName = info.getTemplateTypeName(); typeRegistry.setTemplateTypeName(templateTypeName); } return this; } /** * Add a parameter to the param list. * * @param builder A builder. * @param paramType The parameter type. * @param warnedAboutArgList Whether we've already warned about arg ordering issues (like if * optional args appeared before required ones). * @param isOptional Is this an optional parameter? * @param isVarArgs Is this a var args parameter? * @return Whether a warning was emitted. */ private boolean addParameter( FunctionParamBuilder builder, JSType paramType, boolean warnedAboutArgList, boolean isOptional, boolean isVarArgs) { boolean emittedWarning = false; if (isOptional) { // Remembering that an optional parameter has been encountered // so that if a non optional param is encountered later, an // error can be reported. if (!builder.addOptionalParams(paramType) && !warnedAboutArgList) { reportWarning(VAR_ARGS_MUST_BE_LAST); emittedWarning = true; } } else if (isVarArgs) { if (!builder.addVarArgs(paramType) && !warnedAboutArgList) { reportWarning(VAR_ARGS_MUST_BE_LAST); emittedWarning = true; } } else { if (!builder.addRequiredParams(paramType) && !warnedAboutArgList) { // An optional parameter was seen and this argument is not an optional // or var arg so it is an error. if (builder.hasVarArgs()) { reportWarning(VAR_ARGS_MUST_BE_LAST); } else { reportWarning(OPTIONAL_ARG_AT_END); } emittedWarning = true; } } return emittedWarning; } /** Builds the function type, and puts it in the registry. */ FunctionType buildAndRegister() { if (returnType == null) { returnType = typeRegistry.getNativeType(UNKNOWN_TYPE); } if (parametersNode == null) { throw new IllegalStateException("All Function types must have params and a return type"); } FunctionType fnType; if (isConstructor) { fnType = getOrCreateConstructor(); } else if (isInterface) { fnType = typeRegistry.createInterfaceType(fnName, sourceNode); if (getScopeDeclaredIn().isGlobal() && !fnName.isEmpty()) { typeRegistry.declareType(fnName, fnType.getInstanceType()); } maybeSetBaseType(fnType); } else { fnType = new FunctionBuilder(typeRegistry) .withName(fnName) .withSourceNode(sourceNode) .withParamsNode(parametersNode) .withReturnType(returnType, returnTypeInferred) .withTypeOfThis(thisType) .withTemplateName(templateTypeName) .build(); maybeSetBaseType(fnType); } if (implementedInterfaces != null) { fnType.setImplementedInterfaces(implementedInterfaces); } typeRegistry.clearTemplateTypeName(); return fnType; } private void maybeSetBaseType(FunctionType fnType) { if (baseType != null) { fnType.setPrototypeBasedOn(baseType); } } /** * Returns a constructor function either by returning it from the registry if it exists or * creating and registering a new type. If there is already a type, then warn if the existing type * is different than the one we are creating, though still return the existing function if * possible. The primary purpose of this is that registering a constructor will fail for all * built-in types that are initialized in {@link JSTypeRegistry}. We a) want to make sure that the * type information specified in the externs file matches what is in the registry and b) annotate * the externs with the {@link JSType} from the registry so that there are not two separate JSType * objects for one type. */ private FunctionType getOrCreateConstructor() { FunctionType fnType = typeRegistry.createConstructorType(fnName, sourceNode, parametersNode, returnType); JSType existingType = typeRegistry.getType(fnName); if (existingType != null) { boolean isInstanceObject = existingType instanceof InstanceObjectType; if (isInstanceObject || fnName.equals("Function")) { FunctionType existingFn = isInstanceObject ? ((InstanceObjectType) existingType).getConstructor() : typeRegistry.getNativeFunctionType(FUNCTION_FUNCTION_TYPE); if (existingFn.getSource() == null) { existingFn.setSource(sourceNode); } if (!existingFn.hasEqualCallType(fnType)) { reportWarning(TYPE_REDEFINITION, fnName, fnType.toString(), existingFn.toString()); } return existingFn; } else { // We fall through and return the created type, even though it will fail // to register. We have no choice as we have to return a function. We // issue an error elsewhere though, so the user should fix it. } } maybeSetBaseType(fnType); if (getScopeDeclaredIn().isGlobal() && !fnName.isEmpty()) { typeRegistry.declareType(fnName, fnType.getInstanceType()); } return fnType; } private void reportWarning(DiagnosticType warning, String... args) { compiler.report(JSError.make(sourceName, errorRoot, warning, args)); } private void reportError(DiagnosticType error, String... args) { compiler.report(JSError.make(sourceName, errorRoot, error, args)); } /** Determines whether the given jsdoc info declares a function type. */ static boolean isFunctionTypeDeclaration(JSDocInfo info) { return info.getParameterCount() > 0 || info.hasReturnType() || info.hasThisType() || info.isConstructor() || info.isInterface(); } /** * The scope that we should declare this function in, if it needs to be declared in a scope. * Notice that TypedScopeCreator takes care of most scope-declaring. */ private Scope getScopeDeclaredIn() { int dotIndex = fnName.indexOf("."); if (dotIndex != -1) { String rootVarName = fnName.substring(0, dotIndex); Var rootVar = scope.getVar(rootVarName); if (rootVar != null) { return rootVar.getScope(); } } return scope; } }
/** * Records all of the symbols and properties that should be exported. * * <p>Currently applies to: - function foo() {} - var foo = function() {} - foo.bar = function() {} * - var FOO = ...; - foo.BAR = ...; * * <p>FOO = BAR = 5; and var FOO = BAR = 5; are not supported because the annotation is ambiguous to * whether it applies to all the variables or only the first one. */ class FindExportableNodes extends AbstractPostOrderCallback { static final DiagnosticType NON_GLOBAL_ERROR = DiagnosticType.error( "JSC_NON_GLOBAL_ERROR", "@export only applies to symbols/properties defined in the " + "global scope."); static final DiagnosticType EXPORT_ANNOTATION_NOT_ALLOWED = DiagnosticType.error( "JSC_EXPORT_ANNOTATION_NOT_ALLOWED", "@export is not supported on this expression."); private final AbstractCompiler compiler; /** * It's convenient to be able to iterate over exports in the order in which they are encountered. */ private final LinkedHashMap<String, GenerateNodeContext> exports = new LinkedHashMap<>(); private final boolean allowLocalExports; FindExportableNodes(AbstractCompiler compiler, boolean allowLocalExports) { this.compiler = compiler; this.allowLocalExports = allowLocalExports; } @Override public void visit(NodeTraversal t, Node n, Node parent) { JSDocInfo docInfo = n.getJSDocInfo(); if (docInfo != null && docInfo.isExport()) { if (parent.isAssign() && (n.isFunction() || n.isClass())) { JSDocInfo parentInfo = parent.getJSDocInfo(); if (parentInfo != null && parentInfo.isExport()) { // ScopedAliases produces export annotations on both the function/class // node and assign node, we only want to visit the assign node. return; } } String export = null; GenerateNodeContext context = null; switch (n.getType()) { case Token.FUNCTION: case Token.CLASS: if (parent.isScript()) { export = NodeUtil.getName(n); context = new GenerateNodeContext(n, Mode.EXPORT); } break; case Token.MEMBER_FUNCTION_DEF: export = n.getString(); context = new GenerateNodeContext(n, Mode.EXPORT); break; case Token.ASSIGN: Node grandparent = parent.getParent(); if (parent.isExprResult() && !n.getLastChild().isAssign()) { if (grandparent != null && grandparent.isScript() && n.getFirstChild().isQualifiedName()) { export = n.getFirstChild().getQualifiedName(); context = new GenerateNodeContext(n, Mode.EXPORT); } else if (allowLocalExports && n.getFirstChild().isGetProp()) { Node target = n.getFirstChild(); export = target.getLastChild().getString(); context = new GenerateNodeContext(n, Mode.EXTERN); } } break; case Token.VAR: case Token.LET: case Token.CONST: if (parent.isScript()) { if (n.getFirstChild().hasChildren() && !n.getFirstChild().getFirstChild().isAssign()) { export = n.getFirstChild().getString(); context = new GenerateNodeContext(n, Mode.EXPORT); } } break; case Token.GETPROP: if (allowLocalExports && parent.isExprResult()) { export = n.getLastChild().getString(); context = new GenerateNodeContext(n, Mode.EXTERN); } break; case Token.STRING_KEY: case Token.GETTER_DEF: case Token.SETTER_DEF: if (allowLocalExports) { export = n.getString(); context = new GenerateNodeContext(n, Mode.EXTERN); } break; } if (export != null) { exports.put(export, context); } else { // Don't produce extra warnings for functions values of object literals if (!n.isFunction() || !NodeUtil.isObjectLitKey(parent)) { if (allowLocalExports) { compiler.report(t.makeError(n, EXPORT_ANNOTATION_NOT_ALLOWED)); } else { compiler.report(t.makeError(n, NON_GLOBAL_ERROR)); } } } } } LinkedHashMap<String, GenerateNodeContext> getExports() { return exports; } static enum Mode { EXPORT, EXTERN } /** Context holding the node references required for generating the export calls. */ static class GenerateNodeContext { private final Node node; private final Mode mode; GenerateNodeContext(Node node, Mode mode) { this.node = node; this.mode = mode; } Node getNode() { return node; } public Mode getMode() { return mode; } } }
/** * A compiler pass that verifies the structure of the AST conforms to a number of invariants. * Because this can add a lot of overhead, we only run this in development mode. */ class SanityCheck implements CompilerPass { static final DiagnosticType CANNOT_PARSE_GENERATED_CODE = DiagnosticType.error( "JSC_CANNOT_PARSE_GENERATED_CODE", "Internal compiler error. Cannot parse generated code: {0}"); static final DiagnosticType GENERATED_BAD_CODE = DiagnosticType.error( "JSC_GENERATED_BAD_CODE", "Internal compiler error. Generated bad code." + "----------------------------------------\n" + "Expected:\n{0}\n" + "----------------------------------------\n" + "Actual:\n{1}"); private final AbstractCompiler compiler; SanityCheck(AbstractCompiler compiler) { this.compiler = compiler; } public void process(Node externs, Node root) { sanityCheckNormalization(externs, root); sanityCheckCodeGeneration(root); } /** * Sanity checks code generation by performing it once, parsing the result, then generating code * from the second parse tree to verify that it matches the code generated from the first parse * tree. * * @return The regenerated parse tree. Null on error. */ private Node sanityCheckCodeGeneration(Node root) { if (compiler.hasHaltingErrors()) { // Don't even bother checking code generation if we already know the // the code is bad. return null; } String source = compiler.toSource(root); Node root2 = compiler.parseSyntheticCode(source); if (compiler.hasHaltingErrors()) { compiler.report( JSError.make( CANNOT_PARSE_GENERATED_CODE, Strings.truncateAtMaxLength(source, 100, true))); // Throw an exception, so that the infrastructure will tell us // which pass violated the sanity check. throw new IllegalStateException("Sanity Check failed"); } String source2 = compiler.toSource(root2); if (!source.equals(source2)) { compiler.report( JSError.make( GENERATED_BAD_CODE, Strings.truncateAtMaxLength(source, 1000, true), Strings.truncateAtMaxLength(source2, 1000, true))); // Throw an exception, so that the infrastructure will tell us // which pass violated the sanity check. throw new IllegalStateException("Sanity Check failed"); } return root2; } /** Sanity checks the AST. This is by verifing the normalization passes do nothing. */ private void sanityCheckNormalization(Node externs, Node root) { // Verify nothing has inappropriately denormalize the AST. CodeChangeHandler handler = new CodeChangeHandler.ForbiddenChange(); compiler.addChangeHandler(handler); // TODO(johnlenz): Change these normalization checks Preconditions and // Exceptions into Errors so that it is easier to find the root cause // when there are cascading issues. new PrepareAst(compiler, true).process(null, root); if (compiler.isNormalized()) { (new Normalize(compiler, true)).process(externs, root); boolean checkUserDeclarations = true; CompilerPass pass = new Normalize.VerifyConstants(compiler, checkUserDeclarations); pass.process(externs, root); } compiler.removeChangeHandler(handler); } }
/** * Provides a framework for checking code against a set of user configured conformance rules. The * rules are specified by the ConformanceConfig proto, which allows for both standard checks * (forbidden properties, variables, or dependencies) and allow for more complex checks using custom * rules than specify */ @GwtIncompatible("com.google.protobuf") public final class CheckConformance implements Callback, CompilerPass { static final DiagnosticType CONFORMANCE_VIOLATION = DiagnosticType.warning("JSC_CONFORMANCE_VIOLATION", "Violation: {0}{1}{2}"); static final DiagnosticType CONFORMANCE_POSSIBLE_VIOLATION = DiagnosticType.warning("JSC_CONFORMANCE_POSSIBLE_VIOLATION", "Possible violation: {0}{1}{2}"); static final DiagnosticType INVALID_REQUIREMENT_SPEC = DiagnosticType.error( "JSC_INVALID_REQUIREMENT_SPEC", "Invalid requirement. Reason: {0}\nRequirement spec:\n{1}"); private final AbstractCompiler compiler; private final ImmutableList<Rule> rules; public static interface Rule { /** Perform conformance check */ void check(NodeTraversal t, Node n); } /** @param configs The rules to check. */ CheckConformance(AbstractCompiler compiler, ImmutableList<ConformanceConfig> configs) { this.compiler = compiler; // Initialize the map of functions to inspect for renaming candidates. this.rules = initRules(compiler, configs); } @Override public void process(Node externs, Node root) { if (!rules.isEmpty()) { NodeTraversal.traverseRootsEs6(compiler, this, externs, root); } } @Override public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { // Don't inspect extern files return !n.isScript() || !t.getInput().getSourceFile().isExtern(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { for (int i = 0, len = rules.size(); i < len; i++) { Rule rule = rules.get(i); rule.check(t, n); } } /** Build the data structures need by this pass from the provided configurations. */ private static ImmutableList<Rule> initRules( AbstractCompiler compiler, ImmutableList<ConformanceConfig> configs) { ImmutableList.Builder<Rule> builder = ImmutableList.builder(); List<Requirement> requirements = mergeRequirements(compiler, configs); for (Requirement requirement : requirements) { Rule rule = initRule(compiler, requirement); if (rule != null) { builder.add(rule); } } return builder.build(); } private static final Set<String> EXTENDABLE_FIELDS = ImmutableSet.of( "extends", "whitelist", "whitelist_regexp", "only_apply_to", "only_apply_to_regexp"); /** * Gets requirements from all configs. Merges whitelists of requirements with 'extends' equal to * 'rule_id' of other rule. */ static List<Requirement> mergeRequirements( AbstractCompiler compiler, List<ConformanceConfig> configs) { List<Requirement.Builder> builders = new ArrayList<>(); Map<String, Requirement.Builder> extendable = new HashMap<>(); for (ConformanceConfig config : configs) { for (Requirement requirement : config.getRequirementList()) { Requirement.Builder builder = requirement.toBuilder(); if (requirement.hasRuleId()) { if (requirement.getRuleId().isEmpty()) { reportInvalidRequirement(compiler, requirement, "empty rule_id"); continue; } if (extendable.containsKey(requirement.getRuleId())) { reportInvalidRequirement( compiler, requirement, "two requirements with the same rule_id: " + requirement.getRuleId()); continue; } extendable.put(requirement.getRuleId(), builder); } if (!requirement.hasExtends()) { builders.add(builder); } } } for (ConformanceConfig config : configs) { for (Requirement requirement : config.getRequirementList()) { if (requirement.hasExtends()) { Requirement.Builder existing = extendable.get(requirement.getExtends()); if (existing == null) { reportInvalidRequirement( compiler, requirement, "no requirement with rule_id: " + requirement.getExtends()); continue; } for (Descriptors.FieldDescriptor field : requirement.getAllFields().keySet()) { if (!EXTENDABLE_FIELDS.contains(field.getName())) { reportInvalidRequirement( compiler, requirement, "extending rules allow only " + EXTENDABLE_FIELDS); } } existing.addAllWhitelist(requirement.getWhitelistList()); existing.addAllWhitelistRegexp(requirement.getWhitelistRegexpList()); existing.addAllOnlyApplyTo(requirement.getOnlyApplyToList()); existing.addAllOnlyApplyToRegexp(requirement.getOnlyApplyToRegexpList()); } } } List<Requirement> requirements = new ArrayList<>(builders.size()); for (Requirement.Builder builder : builders) { Requirement requirement = builder.build(); checkRequirementList(compiler, requirement, "whitelist"); checkRequirementList(compiler, requirement, "whitelist_regexp"); checkRequirementList(compiler, requirement, "only_apply_to"); checkRequirementList(compiler, requirement, "only_apply_to_regexp"); requirements.add(requirement); } return requirements; } private static void checkRequirementList( AbstractCompiler compiler, Requirement requirement, String field) { Set<String> existing = new HashSet<>(); for (String value : getRequirementList(requirement, field)) { if (!existing.add(value)) { reportInvalidRequirement(compiler, requirement, "duplicate " + field + " value: " + value); } } } private static List<String> getRequirementList(Requirement requirement, String field) { switch (field) { case "whitelist": return requirement.getWhitelistList(); case "whitelist_regexp": return requirement.getWhitelistRegexpList(); case "only_apply_to": return requirement.getOnlyApplyToList(); case "only_apply_to_regexp": return requirement.getOnlyApplyToRegexpList(); default: throw new AssertionError("Unrecognized field: " + field); } } private static Rule initRule(AbstractCompiler compiler, Requirement requirement) { try { switch (requirement.getType()) { case CUSTOM: return new ConformanceRules.CustomRuleProxy(compiler, requirement); case BANNED_CODE_PATTERN: return new ConformanceRules.BannedCodePattern(compiler, requirement); case BANNED_DEPENDENCY: return new ConformanceRules.BannedDependency(compiler, requirement); case BANNED_NAME: return new ConformanceRules.BannedName(compiler, requirement); case BANNED_PROPERTY: case BANNED_PROPERTY_READ: case BANNED_PROPERTY_WRITE: case BANNED_PROPERTY_CALL: return new ConformanceRules.BannedProperty(compiler, requirement); case RESTRICTED_NAME_CALL: return new ConformanceRules.RestrictedNameCall(compiler, requirement); case RESTRICTED_METHOD_CALL: return new ConformanceRules.RestrictedMethodCall(compiler, requirement); default: reportInvalidRequirement(compiler, requirement, "unknown requirement type"); return null; } } catch (InvalidRequirementSpec e) { reportInvalidRequirement(compiler, requirement, e.getMessage()); return null; } } public static class InvalidRequirementSpec extends Exception { InvalidRequirementSpec(String message) { super(message); } } /** @param requirement */ private static void reportInvalidRequirement( AbstractCompiler compiler, Requirement requirement, String reason) { compiler.report( JSError.make(INVALID_REQUIREMENT_SPEC, reason, TextFormat.printToString(requirement))); } }
/** * The syntactic scope creator scans the parse tree to create a Scope object containing all the * variable declarations in that scope. * * <p>This implementation is not thread-safe. */ class SyntacticScopeCreator implements ScopeCreator { private final AbstractCompiler compiler; private Scope scope; private String sourceName; private final RedeclarationHandler redeclarationHandler; // The arguments variable is special, in that it's declared in every local // scope, but not explicitly declared. private static final String ARGUMENTS = "arguments"; public static final DiagnosticType VAR_MULTIPLY_DECLARED_ERROR = DiagnosticType.error("JSC_VAR_MULTIPLY_DECLARED_ERROR", "Variable {0} first declared in {1}"); public static final DiagnosticType VAR_ARGUMENTS_SHADOWED_ERROR = DiagnosticType.error( "JSC_VAR_ARGUMENTS_SHADOWED_ERROR", "Shadowing \"arguments\" is not allowed"); /** Creates a ScopeCreator. */ SyntacticScopeCreator(AbstractCompiler compiler) { this.compiler = compiler; this.redeclarationHandler = new DefaultRedeclarationHandler(); } SyntacticScopeCreator(AbstractCompiler compiler, RedeclarationHandler redeclarationHandler) { this.compiler = compiler; this.redeclarationHandler = redeclarationHandler; } public Scope createScope(Node n, Scope parent) { sourceName = null; if (parent == null) { scope = new Scope(n, compiler); } else { scope = new Scope(parent, n); } scanRoot(n, parent); sourceName = null; Scope returnedScope = scope; scope = null; return returnedScope; } private void scanRoot(Node n, Scope parent) { if (n.getType() == Token.FUNCTION) { sourceName = (String) n.getProp(Node.SOURCENAME_PROP); final Node fnNameNode = n.getFirstChild(); final Node args = fnNameNode.getNext(); final Node body = args.getNext(); // Bleed the function name into the scope, if it hasn't // been declared in the outer scope. String fnName = fnNameNode.getString(); if (!fnName.isEmpty() && NodeUtil.isFunctionExpression(n)) { declareVar(fnName, fnNameNode, n, null, null, n); } // Args: Declare function variables Preconditions.checkState(args.getType() == Token.LP); for (Node a = args.getFirstChild(); a != null; a = a.getNext()) { Preconditions.checkState(a.getType() == Token.NAME); declareVar(a.getString(), a, args, n, null, n); } // Body scanVars(body, n); } else { // It's the global block Preconditions.checkState(scope.getParent() == null); scanVars(n, null); } } /** Scans and gather variables declarations under a Node */ private void scanVars(Node n, Node parent) { switch (n.getType()) { case Token.VAR: // Declare all variables. e.g. var x = 1, y, z; for (Node child = n.getFirstChild(); child != null; ) { Node next = child.getNext(); Preconditions.checkState(child.getType() == Token.NAME); String name = child.getString(); declareVar(name, child, n, parent, null, n); child = next; } return; case Token.FUNCTION: if (NodeUtil.isFunctionExpression(n)) { return; } String fnName = n.getFirstChild().getString(); if (fnName.isEmpty()) { // This is invalid, but allow it so the checks can catch it. return; } declareVar(fnName, n.getFirstChild(), n, parent, null, n); return; // should not examine function's children case Token.CATCH: Preconditions.checkState(n.getChildCount() == 2); Preconditions.checkState(n.getFirstChild().getType() == Token.NAME); // the first child is the catch var and the third child // is the code block final Node var = n.getFirstChild(); final Node block = var.getNext(); declareVar(var.getString(), var, n, parent, null, n); scanVars(block, n); return; // only one child to scan case Token.SCRIPT: sourceName = (String) n.getProp(Node.SOURCENAME_PROP); break; } // Variables can only occur in statement-level nodes, so // we only need to traverse children in a couple special cases. if (NodeUtil.isControlStructure(n) || NodeUtil.isStatementBlock(n)) { for (Node child = n.getFirstChild(); child != null; ) { Node next = child.getNext(); scanVars(child, n); child = next; } } } /** Interface for injectable duplicate handling. */ interface RedeclarationHandler { void onRedeclaration( Scope s, String name, Node n, Node parent, Node gramps, Node nodeWithLineNumber); } /** The default handler for duplicate declarations. */ private class DefaultRedeclarationHandler implements RedeclarationHandler { public void onRedeclaration( Scope s, String name, Node n, Node parent, Node gramps, Node nodeWithLineNumber) { // Don't allow multiple variables to be declared at the top level scope if (scope.isGlobal()) { Scope.Var origVar = scope.getVar(name); Node origParent = origVar.getParentNode(); if (origParent.getType() == Token.CATCH && parent.getType() == Token.CATCH) { // Okay, both are 'catch(x)' variables. return; } boolean allowDupe = false; JSDocInfo info = n.getJSDocInfo(); if (info == null) { info = parent.getJSDocInfo(); } allowDupe = info != null && info.getSuppressions().contains("duplicate"); if (!allowDupe) { compiler.report( JSError.make( sourceName, nodeWithLineNumber, VAR_MULTIPLY_DECLARED_ERROR, name, (origVar.input != null ? origVar.input.getName() : "??"))); } } else if (name.equals(ARGUMENTS) && !NodeUtil.isVarDeclaration(n)) { // Disallow shadowing "arguments" as we can't handle with our current // scope modeling. compiler.report(JSError.make(sourceName, nodeWithLineNumber, VAR_ARGUMENTS_SHADOWED_ERROR)); } } } /** * Declares a variable. * * @param name The variable name * @param n The node corresponding to the variable name (usually a NAME node) * @param parent The parent node of {@code n} * @param gramps The parent node of {@code parent} * @param declaredType The variable's type, according to JSDoc * @param nodeWithLineNumber The node to use to access the line number of the variable * declaration, if needed */ private void declareVar( String name, Node n, Node parent, Node gramps, JSType declaredType, Node nodeWithLineNumber) { if (scope.isDeclared(name, false) || (scope.isLocal() && name.equals(ARGUMENTS))) { redeclarationHandler.onRedeclaration(scope, name, n, parent, gramps, nodeWithLineNumber); } else { scope.declare(name, n, declaredType, compiler.getInput(sourceName)); } } }
/** * Rewrites "goog.defineClass" into a form that is suitable for type checking and dead code * elimination. * * @author [email protected] (John Lenz) */ class ClosureRewriteClass extends AbstractPostOrderCallback implements HotSwapCompilerPass { // Errors static final DiagnosticType GOOG_CLASS_TARGET_INVALID = DiagnosticType.error( "JSC_GOOG_CLASS_TARGET_INVALID", "Unsupported class definition expression."); static final DiagnosticType GOOG_CLASS_SUPER_CLASS_NOT_VALID = DiagnosticType.error( "JSC_GOOG_CLASS_SUPER_CLASS_NOT_VALID", "The super class must be null or a valid name reference"); static final DiagnosticType GOOG_CLASS_DESCRIPTOR_NOT_VALID = DiagnosticType.error( "JSC_GOOG_CLASS_DESCRIPTOR_NOT_VALID", "The class descriptor must be an object literal"); static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_MISSING = DiagnosticType.error( "JSC_GOOG_CLASS_CONSTRUCTOR_MISSING", "The constructor expression is missing for the class descriptor"); static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE = DiagnosticType.error( "JSC_GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE", "Should not have a constructor expression for an interface"); static final DiagnosticType GOOG_CLASS_STATICS_NOT_VALID = DiagnosticType.error( "JSC_GOOG_CLASS_STATICS_NOT_VALID", "The class statics descriptor must be an object or function literal"); static final DiagnosticType GOOG_CLASS_UNEXPECTED_PARAMS = DiagnosticType.error( "JSC_GOOG_CLASS_UNEXPECTED_PARAMS", "The class definition has too many arguments."); // Warnings static final DiagnosticType GOOG_CLASS_NG_INJECT_ON_CLASS = DiagnosticType.warning( "JSC_GOOG_CLASS_NG_INJECT_ON_CLASS", "@ngInject should be declared on the constructor, not on the class."); private final AbstractCompiler compiler; public ClosureRewriteClass(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { hotSwapScript(root, null); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverse(compiler, scriptRoot, this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isCall() && isGoogDefineClass(n)) { if (!validateUsage(n)) { compiler.report(JSError.make(n, GOOG_CLASS_TARGET_INVALID)); } } maybeRewriteClassDefinition(n); } private boolean validateUsage(Node n) { // There are only three valid usage patterns for of goog.defineClass // var ClassName = googDefineClass // namespace.ClassName = googDefineClass // and within an objectlit, used by the goog.defineClass. Node parent = n.getParent(); switch (parent.getType()) { case Token.NAME: return true; case Token.ASSIGN: return n == parent.getLastChild() && parent.getParent().isExprResult(); case Token.STRING_KEY: return isContainedInGoogDefineClass(parent); } return false; } private boolean isContainedInGoogDefineClass(Node n) { while (n != null) { n = n.getParent(); if (n.isCall()) { if (isGoogDefineClass(n)) { return true; } } else if (!n.isObjectLit() && !n.isStringKey()) { break; } } return false; } private void maybeRewriteClassDefinition(Node n) { if (n.isVar()) { Node target = n.getFirstChild(); Node value = target.getFirstChild(); maybeRewriteClassDefinition(n, target, value); } else if (NodeUtil.isExprAssign(n)) { Node assign = n.getFirstChild(); Node target = assign.getFirstChild(); Node value = assign.getLastChild(); maybeRewriteClassDefinition(n, target, value); } } private void maybeRewriteClassDefinition(Node n, Node target, Node value) { if (isGoogDefineClass(value)) { if (!target.isQualifiedName()) { compiler.report(JSError.make(n, GOOG_CLASS_TARGET_INVALID)); } ClassDefinition def = extractClassDefinition(target, value); if (def != null) { value.detachFromParent(); target.detachFromParent(); rewriteGoogDefineClass(n, def); } } } private static class MemberDefinition { final JSDocInfo info; final Node name; final Node value; MemberDefinition(JSDocInfo info, Node name, Node value) { this.info = info; this.name = name; this.value = value; } } private static final class ClassDefinition { final Node name; final JSDocInfo classInfo; final Node superClass; final MemberDefinition constructor; final List<MemberDefinition> staticProps; final List<MemberDefinition> props; final Node classModifier; ClassDefinition( Node name, JSDocInfo classInfo, Node superClass, MemberDefinition constructor, List<MemberDefinition> staticProps, List<MemberDefinition> props, Node classModifier) { this.name = name; this.classInfo = classInfo; this.superClass = superClass; this.constructor = constructor; this.staticProps = staticProps; this.props = props; this.classModifier = classModifier; } } /** * Validates the class definition and if valid, destructively extracts the class definition from * the AST. */ private ClassDefinition extractClassDefinition(Node targetName, Node callNode) { JSDocInfo classInfo = NodeUtil.getBestJSDocInfo(targetName); // name = goog.defineClass(superClass, {...}, [modifier, ...]) Node superClass = NodeUtil.getArgumentForCallOrNew(callNode, 0); if (superClass == null || (!superClass.isNull() && !superClass.isQualifiedName())) { compiler.report(JSError.make(callNode, GOOG_CLASS_SUPER_CLASS_NOT_VALID)); return null; } if (NodeUtil.isNullOrUndefined(superClass) || superClass.matchesQualifiedName("Object")) { superClass = null; } Node description = NodeUtil.getArgumentForCallOrNew(callNode, 1); if (description == null || !description.isObjectLit() || !validateObjLit(description)) { // report bad class definition compiler.report(JSError.make(callNode, GOOG_CLASS_DESCRIPTOR_NOT_VALID)); return null; } int paramCount = callNode.getChildCount() - 1; if (paramCount > 2) { compiler.report(JSError.make(callNode, GOOG_CLASS_UNEXPECTED_PARAMS)); return null; } Node constructor = extractProperty(description, "constructor"); if (classInfo != null && classInfo.isInterface()) { if (constructor != null) { compiler.report(JSError.make(description, GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE)); return null; } } else if (constructor == null) { // report missing constructor compiler.report(JSError.make(description, GOOG_CLASS_CONSTRUCTOR_MISSING)); return null; } if (constructor == null) { constructor = IR.function( IR.name("").srcref(callNode), IR.paramList().srcref(callNode), IR.block().srcref(callNode)); constructor.srcref(callNode); } JSDocInfo info = NodeUtil.getBestJSDocInfo(constructor); Node classModifier = null; Node statics = null; Node staticsProp = extractProperty(description, "statics"); if (staticsProp != null) { if (staticsProp.isObjectLit() && validateObjLit(staticsProp)) { statics = staticsProp; } else if (staticsProp.isFunction()) { classModifier = staticsProp; } else { compiler.report(JSError.make(staticsProp, GOOG_CLASS_STATICS_NOT_VALID)); return null; } } if (statics == null) { statics = IR.objectlit(); } // Ok, now rip apart the definition into its component pieces. // Remove the "special" property key nodes. maybeDetach(constructor.getParent()); maybeDetach(statics.getParent()); if (classModifier != null) { maybeDetach(classModifier.getParent()); } ClassDefinition def = new ClassDefinition( targetName, classInfo, maybeDetach(superClass), new MemberDefinition(info, null, maybeDetach(constructor)), objectLitToList(maybeDetach(statics)), objectLitToList(description), maybeDetach(classModifier)); return def; } private static Node maybeDetach(Node node) { if (node != null && node.getParent() != null) { node.detachFromParent(); } return node; } // Only unquoted plain properties are currently supported. private static boolean validateObjLit(Node objlit) { for (Node key : objlit.children()) { if (!key.isStringKey() || key.isQuotedString()) { return false; } } return true; } /** @return The first property in the objlit that matches the key. */ private static Node extractProperty(Node objlit, String keyName) { for (Node keyNode : objlit.children()) { if (keyNode.getString().equals(keyName)) { return keyNode.isStringKey() ? keyNode.getFirstChild() : null; } } return null; } private static List<MemberDefinition> objectLitToList(Node objlit) { List<MemberDefinition> result = Lists.newArrayList(); for (Node keyNode : objlit.children()) { result.add( new MemberDefinition( NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.removeFirstChild())); } objlit.detachChildren(); return result; } private void rewriteGoogDefineClass(Node exprRoot, final ClassDefinition cls) { // For simplicity add everything into a block, before adding it to the AST. Node block = IR.block(); // remove the original jsdoc info if it was attached to the value. cls.constructor.value.setJSDocInfo(null); if (exprRoot.isVar()) { // example: var ctr = function(){} Node var = IR.var(cls.name.cloneTree(), cls.constructor.value).srcref(exprRoot); JSDocInfo mergedClassInfo = mergeJsDocFor(cls, var); var.setJSDocInfo(mergedClassInfo); block.addChildToBack(var); } else { // example: ns.ctr = function(){} Node assign = IR.assign(cls.name.cloneTree(), cls.constructor.value) .srcref(exprRoot) .setJSDocInfo(cls.constructor.info); JSDocInfo mergedClassInfo = mergeJsDocFor(cls, assign); assign.setJSDocInfo(mergedClassInfo); Node expr = IR.exprResult(assign).srcref(exprRoot); block.addChildToBack(expr); } if (cls.superClass != null) { // example: goog.inherits(ctr, superClass) block.addChildToBack( fixupSrcref( IR.exprResult( IR.call( NodeUtil.newQName(compiler, "goog.inherits").srcrefTree(cls.superClass), cls.name.cloneTree(), cls.superClass.cloneTree()) .srcref(cls.superClass)))); } for (MemberDefinition def : cls.staticProps) { // remove the original jsdoc info if it was attached to the value. def.value.setJSDocInfo(null); // example: ctr.prop = value block.addChildToBack( fixupSrcref( IR.exprResult( fixupSrcref( IR.assign( IR.getprop( cls.name.cloneTree(), IR.string(def.name.getString()).srcref(def.name)) .srcref(def.name), def.value)) .setJSDocInfo(def.info)))); // Handle inner class definitions. maybeRewriteClassDefinition(block.getLastChild()); } for (MemberDefinition def : cls.props) { // remove the original jsdoc info if it was attached to the value. def.value.setJSDocInfo(null); // example: ctr.prototype.prop = value block.addChildToBack( fixupSrcref( IR.exprResult( fixupSrcref( IR.assign( IR.getprop( fixupSrcref( IR.getprop( cls.name.cloneTree(), IR.string("prototype").srcref(def.name))), IR.string(def.name.getString()).srcref(def.name)) .srcref(def.name), def.value)) .setJSDocInfo(def.info)))); // Handle inner class definitions. maybeRewriteClassDefinition(block.getLastChild()); } if (cls.classModifier != null) { // Inside the modifier function, replace references to the argument // with the class name. // function(cls) { cls.Foo = bar; } // becomes // function(cls) { theClassName.Foo = bar; } // The cls parameter is unused, but leave it there so that it // matches the JsDoc. // TODO(tbreisacher): Add a warning if the param is shadowed or reassigned. Node argList = cls.classModifier.getFirstChild().getNext(); Node arg = argList.getFirstChild(); final String argName = arg.getString(); NodeTraversal.traverse( compiler, cls.classModifier.getLastChild(), new AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName() && n.getString().equals(argName)) { parent.replaceChild(n, cls.name.cloneTree()); } } }); block.addChildToBack( IR.exprResult( fixupFreeCall( IR.call(cls.classModifier, cls.name.cloneTree()).srcref(cls.classModifier))) .srcref(cls.classModifier)); } Node parent = exprRoot.getParent(); Node stmts = block.removeChildren(); parent.addChildrenAfter(stmts, exprRoot); parent.removeChild(exprRoot); compiler.reportCodeChange(); } private static Node fixupSrcref(Node node) { node.srcref(node.getFirstChild()); return node; } private static Node fixupFreeCall(Node call) { Preconditions.checkState(call.isCall()); call.putBooleanProp(Node.FREE_CALL, true); return call; } /** @return Whether the call represents a class definition. */ private static boolean isGoogDefineClass(Node value) { if (value != null && value.isCall()) { return value.getFirstChild().matchesQualifiedName("goog.defineClass"); } return false; } static final String VIRTUAL_FILE = "<ClosureRewriteClass.java>"; private JSDocInfo mergeJsDocFor(ClassDefinition cls, Node associatedNode) { // avoid null checks JSDocInfo classInfo = (cls.classInfo != null) ? cls.classInfo : new JSDocInfo(true); JSDocInfo ctorInfo = (cls.constructor.info != null) ? cls.constructor.info : new JSDocInfo(true); Node superNode = cls.superClass; // Start with a clone of the constructor info if there is one. JSDocInfoBuilder mergedInfo = cls.constructor.info != null ? JSDocInfoBuilder.copyFrom(ctorInfo) : new JSDocInfoBuilder(true); // merge block description String blockDescription = Joiner.on("\n") .skipNulls() .join(classInfo.getBlockDescription(), ctorInfo.getBlockDescription()); if (!blockDescription.isEmpty()) { mergedInfo.recordBlockDescription(blockDescription); } // merge suppressions Set<String> suppressions = Sets.newHashSet(); suppressions.addAll(classInfo.getSuppressions()); suppressions.addAll(ctorInfo.getSuppressions()); if (!suppressions.isEmpty()) { mergedInfo.recordSuppressions(suppressions); } // Use class deprecation if set. if (classInfo.isDeprecated()) { mergedInfo.recordDeprecated(); } String deprecationReason = null; if (classInfo.getDeprecationReason() != null) { deprecationReason = classInfo.getDeprecationReason(); mergedInfo.recordDeprecationReason(deprecationReason); } // Use class visibility if specifically set Visibility visibility = classInfo.getVisibility(); if (visibility != null && visibility != JSDocInfo.Visibility.INHERITED) { mergedInfo.recordVisibility(classInfo.getVisibility()); } if (classInfo.isConstant()) { mergedInfo.recordConstancy(); } if (classInfo.isExport()) { mergedInfo.recordExport(); } // If @ngInject is on the ctor, it's already been copied above. if (classInfo.isNgInject()) { compiler.report(JSError.make(associatedNode, GOOG_CLASS_NG_INJECT_ON_CLASS)); mergedInfo.recordNgInject(true); } // @constructor is implied, @interface must be explicit boolean isInterface = classInfo.isInterface() || ctorInfo.isInterface(); if (isInterface) { mergedInfo.recordInterface(); List<JSTypeExpression> extendedInterfaces = null; if (classInfo.getExtendedInterfacesCount() > 0) { extendedInterfaces = classInfo.getExtendedInterfaces(); } else if (ctorInfo.getExtendedInterfacesCount() == 0 && superNode != null) { extendedInterfaces = ImmutableList.of( new JSTypeExpression( new Node(Token.BANG, IR.string(superNode.getQualifiedName())), VIRTUAL_FILE)); } if (extendedInterfaces != null) { for (JSTypeExpression extend : extendedInterfaces) { mergedInfo.recordExtendedInterface(extend); } } } else { // @constructor by default mergedInfo.recordConstructor(); if (classInfo.makesUnrestricted() || ctorInfo.makesUnrestricted()) { mergedInfo.recordUnrestricted(); } else if (classInfo.makesDicts() || ctorInfo.makesDicts()) { mergedInfo.recordDict(); } else { // @struct by default mergedInfo.recordStruct(); } if (classInfo.getBaseType() != null) { mergedInfo.recordBaseType(classInfo.getBaseType()); } else if (superNode != null) { // a "super" implies @extends, build a default. JSTypeExpression baseType = new JSTypeExpression( new Node(Token.BANG, IR.string(superNode.getQualifiedName())), VIRTUAL_FILE); mergedInfo.recordBaseType(baseType); } // @implements from the class if they exist List<JSTypeExpression> interfaces = classInfo.getImplementedInterfaces(); for (JSTypeExpression implemented : interfaces) { mergedInfo.recordImplementedInterface(implemented); } } // merge @template types if they exist List<String> templateNames = new ArrayList<>(); templateNames.addAll(classInfo.getTemplateTypeNames()); templateNames.addAll(ctorInfo.getTemplateTypeNames()); for (String typeName : templateNames) { mergedInfo.recordTemplateTypeName(typeName); } return mergedInfo.build(associatedNode); } }
/** * Checks that goog.module() is used correctly. * * <p>Note that this file only does checks that can be done per-file. Whole program checks happen * during goog.module rewriting, in {@link ClosureRewriteModule}. */ public final class ClosureCheckModule implements Callback, HotSwapCompilerPass { static final DiagnosticType MULTIPLE_MODULES_IN_FILE = DiagnosticType.error( "JSC_MULTIPLE_MODULES_IN_FILE", "There should only be a single goog.module() statement per file."); static final DiagnosticType MODULE_AND_PROVIDES = DiagnosticType.error( "JSC_MODULE_AND_PROVIDES", "A file using goog.module() may not also use goog.provide() statements."); static final DiagnosticType GOOG_MODULE_REFERENCES_THIS = DiagnosticType.error( "JSC_GOOG_MODULE_REFERENCES_THIS", "The body of a goog.module cannot reference 'this'."); static final DiagnosticType GOOG_MODULE_USES_THROW = DiagnosticType.error( "JSC_GOOG_MODULE_USES_THROW", "The body of a goog.module cannot use 'throw'."); static final DiagnosticType REQUIRE_NOT_AT_TOP_LEVEL = DiagnosticType.error( "JSC_REQUIRE_NOT_AT_TOP_LEVEL", "goog.require() must be called at file scope."); static final DiagnosticType ONE_REQUIRE_PER_DECLARATION = DiagnosticType.error( "JSC_ONE_REQUIRE_PER_DECLARATION", "There may only be one goog.require() per var/let/const declaration."); private final AbstractCompiler compiler; private Node currentModule = null; public ClosureCheckModule(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverseEs6(compiler, scriptRoot, this); } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { if (n.isScript()) { return NodeUtil.isModuleFile(n); } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.CALL: Node callee = n.getFirstChild(); if (callee.matchesQualifiedName("goog.module")) { if (currentModule == null) { currentModule = n; } else { t.report(n, MULTIPLE_MODULES_IN_FILE); } } else if (callee.matchesQualifiedName("goog.provide")) { t.report(n, MODULE_AND_PROVIDES); } else if (callee.matchesQualifiedName("goog.require")) { checkRequireCall(t, n, parent); } break; case Token.THIS: if (t.inGlobalHoistScope()) { t.report(n, GOOG_MODULE_REFERENCES_THIS); } break; case Token.THROW: if (t.inGlobalHoistScope()) { t.report(n, GOOG_MODULE_USES_THROW); } break; case Token.SCRIPT: currentModule = null; break; } } private void checkRequireCall(NodeTraversal t, Node callNode, Node parent) { Preconditions.checkState(callNode.isCall()); switch (parent.getType()) { case Token.EXPR_RESULT: return; case Token.GETPROP: if (parent.getParent().isName()) { checkRequireCall(t, callNode, parent.getParent()); return; } break; case Token.NAME: case Token.OBJECT_PATTERN: { Node declaration = parent.getParent(); if (declaration.getChildCount() != 1) { t.report(declaration, ONE_REQUIRE_PER_DECLARATION); } return; } } t.report(callNode, REQUIRE_NOT_AT_TOP_LEVEL); } }