public void testReport() { final List<JSError> errors = new ArrayList<JSError>(); Compiler compiler = new Compiler( new BasicErrorManager() { @Override public void report(CheckLevel level, JSError error) { errors.add(error); } @Override public void println(CheckLevel level, JSError error) {} @Override protected void printSummary() {} }); compiler.initCompilerOptionsIfTesting(); NodeTraversal t = new NodeTraversal(compiler, null); DiagnosticType dt = DiagnosticType.warning("FOO", "{0}, {1} - {2}"); t.report(null, dt, "Foo", "Bar", "Hello"); assertEquals(1, errors.size()); assertEquals("Foo, Bar - Hello", errors.get(0).description); }
/** * 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)); } }
/** * 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))); } }
/** * 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)); } } }
/** * 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)); } }
/** * 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); } } }
/** * This pass walks the AST to create a Collection of 'new' nodes and 'goog.require' nodes. It * reconciles these Collections, creating a warning for each discrepancy. * * <p>The rules on when a warning is reported are: * * <ul> * <li>Type is referenced in code -> goog.require is required (missingRequires check fails if it's * not there) * <li>Type is referenced in an @extends or @implements -> goog.require is required * (missingRequires check fails if it's not there) * <li>Type is referenced in other JsDoc (@type etc) -> goog.require is optional (don't warn, * regardless of if it is there) * <li>Type is not referenced at all -> goog.require is forbidden (extraRequires check fails if it * is there) * </ul> */ class CheckRequiresForConstructors implements HotSwapCompilerPass, NodeTraversal.Callback { private final AbstractCompiler compiler; private final CodingConvention codingConvention; public static enum Mode { // Looking at a single file. Only a minimal set of externs are present. SINGLE_FILE, // Used during a normal compilation. The entire program + externs are available. FULL_COMPILE }; private final Mode mode; private final Set<String> providedNames = new HashSet<>(); private final Map<String, Node> requires = new HashMap<>(); // Only used in single-file mode. private final Set<String> closurizedNamespaces = new HashSet<>(); // Adding an entry to usages indicates that the name is used and should be required. private final Map<String, Node> usages = new HashMap<>(); // Adding an entry to weakUsages indicates that the name is used, but in a way which may not // require a goog.require, such as in a @type annotation. If the only usages of a name are // in weakUsages, don't give a missingRequire warning, nor an extraRequire warning. private final Map<String, Node> weakUsages = new HashMap<>(); static final DiagnosticType MISSING_REQUIRE_WARNING = DiagnosticType.disabled("JSC_MISSING_REQUIRE_WARNING", "missing require: ''{0}''"); // Essentially the same as MISSING_REQUIRE_WARNING except that if the user calls foo.bar.baz() // then we don't know whether they should require it as goog.require('foo.bar.baz') or as // goog.require('foo.bar'). So, warn but don't provide a suggested fix. static final DiagnosticType MISSING_REQUIRE_CALL_WARNING = DiagnosticType.disabled( "JSC_MISSING_REQUIRE_CALL_WARNING", "No matching require found for ''{0}''"); static final DiagnosticType EXTRA_REQUIRE_WARNING = DiagnosticType.disabled("JSC_EXTRA_REQUIRE_WARNING", "extra require: ''{0}''"); static final DiagnosticType DUPLICATE_REQUIRE_WARNING = DiagnosticType.disabled("JSC_DUPLICATE_REQUIRE_WARNING", "''{0}'' required more than once."); private static final Set<String> DEFAULT_EXTRA_NAMESPACES = ImmutableSet.of("goog.testing.asserts", "goog.testing.jsunit"); CheckRequiresForConstructors(AbstractCompiler compiler, Mode mode) { this.compiler = compiler; this.mode = mode; this.codingConvention = compiler.getCodingConvention(); } /** * Uses Collections of new and goog.require nodes to create a compiler warning for each new class * name without a corresponding goog.require(). */ @Override public void process(Node externs, Node root) { NodeTraversal.traverseRootsEs6(compiler, this, externs, root); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverseEs6(compiler, scriptRoot, this); } // Return true if the name is a class name (starts with an uppercase // character). This also matches for all-caps constants, which eliminates // some false positives (e.g. goog.LOCALE.replace()). private static boolean isClassName(String name) { return name != null && name.length() > 1 && Character.isUpperCase(name.charAt(0)); } // Return the shortest prefix of the className that refers to a class, // or null if no part refers to a class. private static String getOutermostClassName(String className) { for (String part : Splitter.on('.').split(className)) { if (isClassName(part)) { return className.substring(0, className.indexOf(part) + part.length()); } } return null; } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return parent == null || !parent.isScript() || !t.getInput().isExtern(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { maybeAddJsDocUsages(t, n); switch (n.getType()) { case Token.ASSIGN: case Token.VAR: case Token.LET: case Token.CONST: maybeAddProvidedName(n); break; case Token.FUNCTION: // Exclude function expressions. if (NodeUtil.isStatement(n)) { maybeAddProvidedName(n); } break; case Token.NAME: if (!NodeUtil.isLValue(n)) { visitQualifiedName(n); } break; case Token.GETPROP: visitQualifiedName(n); break; case Token.CALL: visitCallNode(t, n, parent); break; case Token.SCRIPT: visitScriptNode(t); reset(); break; case Token.NEW: visitNewNode(t, n); break; case Token.CLASS: visitClassNode(t, n); break; case Token.IMPORT: visitImportNode(n); break; } } private void reset() { this.usages.clear(); this.weakUsages.clear(); this.requires.clear(); this.closurizedNamespaces.clear(); this.providedNames.clear(); } private void visitScriptNode(NodeTraversal t) { if (mode == Mode.SINGLE_FILE && requires.isEmpty()) { // Likely a file that isn't using Closure at all. return; } Set<String> namespaces = new HashSet<>(); // For every usage, check that there is a goog.require, and warn if not. for (Map.Entry<String, Node> entry : usages.entrySet()) { String namespace = entry.getKey(); if (namespace.endsWith(".call") || namespace.endsWith(".apply")) { namespace = namespace.substring(0, namespace.lastIndexOf('.')); } if (namespace.startsWith("goog.global.")) { continue; } Node node = entry.getValue(); JSDocInfo info = NodeUtil.getBestJSDocInfo(NodeUtil.getEnclosingStatement(node)); if (info != null && info.getSuppressions().contains("missingRequire")) { continue; } String outermostClassName = getOutermostClassName(namespace); // The parent namespace is also checked as part of the requires so that classes // used by goog.module are still checked properly. This may cause missing requires // to be missed but in practice that should happen rarely. String nonNullClassName = outermostClassName != null ? outermostClassName : namespace; String parentNamespace = null; int separatorIndex = nonNullClassName.lastIndexOf('.'); if (separatorIndex > 0) { parentNamespace = nonNullClassName.substring(0, separatorIndex); } boolean notProvidedByConstructors = !providedNames.contains(namespace) && !providedNames.contains(outermostClassName) && !providedNames.contains(parentNamespace); boolean notProvidedByRequires = !requires.containsKey(namespace) && !requires.containsKey(outermostClassName) && !requires.containsKey(parentNamespace); if (notProvidedByConstructors && notProvidedByRequires && !namespaces.contains(namespace) && !"goog".equals(parentNamespace)) { // TODO(mknichel): If the symbol is not explicitly provided, find the next best // symbol from the provides in the same file. String rootName = Splitter.on('.').split(namespace).iterator().next(); if (mode != Mode.SINGLE_FILE || closurizedNamespaces.contains(rootName)) { if (node.isCall()) { compiler.report(t.makeError(node, MISSING_REQUIRE_CALL_WARNING, namespace)); } else { compiler.report(t.makeError(node, MISSING_REQUIRE_WARNING, namespace)); } namespaces.add(namespace); } } } // For every goog.require, check that there is a usage (in either usages or weakUsages) // and warn if there is not. for (Map.Entry<String, Node> entry : requires.entrySet()) { String require = entry.getKey(); Node call = entry.getValue(); Node parent = call.getParent(); if (parent.isAssign()) { // var baz = goog.require('foo.bar.baz'); // Assume that the var 'baz' is used somewhere, and don't warn. continue; } if (!usages.containsKey(require) && !weakUsages.containsKey(require)) { reportExtraRequireWarning(call, require); } } } private void reportExtraRequireWarning(Node call, String require) { if (DEFAULT_EXTRA_NAMESPACES.contains(require)) { return; } JSDocInfo jsDoc = call.getJSDocInfo(); if (jsDoc != null && jsDoc.getSuppressions().contains("extraRequire")) { // There is a @suppress {extraRequire} on the call node. Even though the compiler generally // doesn't understand @suppress in that position, respect it in this case, // since lots of people put it there to suppress the closure-linter's extraRequire check. return; } compiler.report(JSError.make(call, EXTRA_REQUIRE_WARNING, require)); } private void reportDuplicateRequireWarning(Node call, String require) { compiler.report(JSError.make(call, DUPLICATE_REQUIRE_WARNING, require)); } private void visitRequire(String requiredName, Node node) { if (requires.containsKey(requiredName)) { reportDuplicateRequireWarning(node, requiredName); } else { requires.put(requiredName, node); if (mode == Mode.SINGLE_FILE) { String rootName = Splitter.on('.').split(requiredName).iterator().next(); closurizedNamespaces.add(rootName); } } } private void visitImportNode(Node importNode) { Node defaultImport = importNode.getFirstChild(); if (defaultImport.isName()) { visitRequire(defaultImport.getString(), importNode); } Node namedImports = defaultImport.getNext(); if (namedImports.getType() == Token.IMPORT_SPECS) { for (Node importSpec : namedImports.children()) { visitRequire(importSpec.getLastChild().getString(), importNode); } } } private void visitCallNode(NodeTraversal t, Node call, Node parent) { String required = codingConvention.extractClassNameIfRequire(call, parent); if (required != null) { visitRequire(required, call); return; } String provided = codingConvention.extractClassNameIfProvide(call, parent); if (provided != null) { providedNames.add(provided); return; } if (codingConvention.isClassFactoryCall(call)) { if (parent.isName()) { providedNames.add(parent.getString()); } else if (parent.isAssign()) { providedNames.add(parent.getFirstChild().getQualifiedName()); } } Node callee = call.getFirstChild(); if (callee.isName()) { weakUsages.put(callee.getString(), callee); } else if (callee.isQualifiedName()) { Node root = NodeUtil.getRootOfQualifiedName(callee); if (root.isName()) { Var var = t.getScope().getVar(root.getString()); if (var == null || (!var.isExtern() && !var.isLocal())) { String name = getOutermostClassName(callee.getQualifiedName()); if (name == null) { name = callee.getQualifiedName(); } usages.put(name, call); } } } } private void visitQualifiedName(Node getprop) { // For "foo.bar.baz.qux" add weak usages for "foo.bar.baz.qux", "foo.bar.baz", // "foo.bar", and "foo" because those might all be goog.provide'd in different files, // so it doesn't make sense to require the user to goog.require all of them. for (; getprop != null; getprop = getprop.getFirstChild()) { weakUsages.put(getprop.getQualifiedName(), getprop); } } private void visitNewNode(NodeTraversal t, Node newNode) { Node qNameNode = newNode.getFirstChild(); // Single names are likely external, but if this is running in single-file mode, they // will not be in the externs, so add a weak usage. if (mode == Mode.SINGLE_FILE && qNameNode.isName()) { weakUsages.put(qNameNode.getString(), qNameNode); return; } // If the ctor is something other than a qualified name, ignore it. if (!qNameNode.isQualifiedName()) { return; } // Grab the root ctor namespace. Node root = NodeUtil.getRootOfQualifiedName(qNameNode); // We only consider programmer-defined constructors that are // global variables, or are defined on global variables. if (!root.isName()) { return; } String name = root.getString(); Var var = t.getScope().getVar(name); if (var != null && (var.isExtern() || var.getSourceFile() == newNode.getStaticSourceFile())) { return; } usages.put(qNameNode.getQualifiedName(), newNode); // for "new foo.bar.Baz.Qux" add weak usages for "foo.bar.Baz", "foo.bar", and "foo" // because those might be goog.provide'd from a different file than foo.bar.Baz.Qux, // so it doesn't make sense to require the user to goog.require all of them. for (; qNameNode != null; qNameNode = qNameNode.getFirstChild()) { weakUsages.put(qNameNode.getQualifiedName(), qNameNode); } } private void visitClassNode(NodeTraversal t, Node classNode) { String name = NodeUtil.getName(classNode); if (name != null) { providedNames.add(name); } Node extendClass = classNode.getSecondChild(); // If the superclass is something other than a qualified name, ignore it. if (!extendClass.isQualifiedName()) { return; } // Single names are likely external, but if this is running in single-file mode, they // will not be in the externs, so add a weak usage. if (mode == Mode.SINGLE_FILE && extendClass.isName()) { weakUsages.put(extendClass.getString(), extendClass); return; } Node root = NodeUtil.getRootOfQualifiedName(extendClass); // It should always be a name. Extending this.something or // super.something is unlikely. // We only consider programmer-defined superclasses that are // global variables, or are defined on global variables. if (root.isName()) { String rootName = root.getString(); Var var = t.getScope().getVar(rootName); if (var != null && (var.isLocal() || var.isExtern())) { // "require" not needed for these } else { usages.put(extendClass.getQualifiedName(), extendClass); } } } private void maybeAddProvidedName(Node n) { Node name = n.getFirstChild(); if (name.isQualifiedName()) { providedNames.add(name.getQualifiedName()); } } /** * If this returns true, check for @extends and @implements annotations on this node. Otherwise, * it's probably an alias for an existing class, so skip those annotations. * * @return Whether the given node declares a function. True for the following forms: * <li> * <pre>function foo() {}</pre> * <li> * <pre>var foo = function() {};</pre> * <li> * <pre>foo.bar = function() {};</pre> */ private boolean declaresFunction(Node n) { if (n.isFunction()) { return true; } if (n.isAssign() && n.getLastChild().isFunction()) { return true; } if (NodeUtil.isNameDeclaration(n) && n.getFirstChild().hasChildren() && n.getFirstFirstChild().isFunction()) { return true; } return false; } private void maybeAddJsDocUsages(NodeTraversal t, Node n) { JSDocInfo info = n.getJSDocInfo(); if (info == null) { return; } if (declaresFunction(n)) { for (JSTypeExpression expr : info.getImplementedInterfaces()) { maybeAddUsage(t, n, expr); } if (info.getBaseType() != null) { maybeAddUsage(t, n, info.getBaseType()); } for (JSTypeExpression extendedInterface : info.getExtendedInterfaces()) { maybeAddUsage(t, n, extendedInterface); } } for (Node typeNode : info.getTypeNodes()) { maybeAddWeakUsage(t, n, typeNode); } } /** * Adds a weak usage for the given type expression (unless it references a variable that is * defined in the externs, in which case no goog.require() is needed). When a "weak usage" is * added, it means that a goog.require for that type is optional: No warning is given whether the * require is there or not. */ private void maybeAddWeakUsage(NodeTraversal t, Node n, Node typeNode) { maybeAddUsage(t, n, typeNode, this.weakUsages, Predicates.<Node>alwaysTrue()); } /** * Adds a usage for the given type expression (unless it references a variable that is defined in * the externs, in which case no goog.require() is needed). When a usage is added, it means that * there should be a goog.require for that type. */ private void maybeAddUsage(NodeTraversal t, Node n, final JSTypeExpression expr) { // Just look at the root node, don't traverse. Predicate<Node> pred = new Predicate<Node>() { @Override public boolean apply(Node n) { return n == expr.getRoot(); } }; maybeAddUsage(t, n, expr.getRoot(), this.usages, pred); } private void maybeAddUsage( final NodeTraversal t, final Node n, Node rootTypeNode, final Map<String, Node> usagesMap, Predicate<Node> pred) { Visitor visitor = new Visitor() { @Override public void visit(Node typeNode) { if (typeNode.isString()) { String typeString = typeNode.getString(); if (mode == Mode.SINGLE_FILE && !typeString.contains(".")) { // If using a single-name type, it's probably something like Error, which we // don't have externs for. weakUsages.put(typeString, n); return; } String rootName = Splitter.on('.').split(typeString).iterator().next(); Var var = t.getScope().getVar(rootName); if (var == null || !var.isExtern()) { usagesMap.put(typeString, n); // Regardless of whether we're adding a weak or strong usage here, add weak usages // for the prefixes of the namespace, like we do for GETPROP nodes. Otherwise we get // an extra require warning for cases like: // // goog.require('foo.bar.SomeService'); // // /** @constructor @extends {foo.bar.SomeService.Handler} */ // var MyHandler = function() {}; Node getprop = NodeUtil.newQName(compiler, typeString); getprop.useSourceInfoIfMissingFromForTree(typeNode); visitQualifiedName(getprop); } else { // Even if the root namespace is in externs, add a weak usage because the full // namespace may still be goog.provided. weakUsages.put(typeString, n); } } } }; NodeUtil.visitPreOrder(rootTypeNode, visitor, pred); } }
/** * Checks for common errors, such as misplaced semicolons: * * <pre> * if (x); act_now(); * </pre> * * or comparison against NaN: * * <pre> * if (x === NaN) act(); * </pre> * * and generates warnings. * * @author [email protected] (John Lenz) */ final class CheckSuspiciousCode extends AbstractPostOrderCallback { static final DiagnosticType SUSPICIOUS_SEMICOLON = DiagnosticType.warning( "JSC_SUSPICIOUS_SEMICOLON", "If this if/for/while really shouldn''t have a body, use '{}'"); static final DiagnosticType SUSPICIOUS_COMPARISON_WITH_NAN = DiagnosticType.warning( "JSC_SUSPICIOUS_NAN", "Comparison against NaN is always false. Did you mean isNaN()?"); static final DiagnosticType SUSPICIOUS_IN_OPERATOR = DiagnosticType.warning( "JSC_SUSPICIOUS_IN", "Use of the \"in\" keyword on non-object types throws an exception."); static final DiagnosticType SUSPICIOUS_INSTANCEOF_LEFT_OPERAND = DiagnosticType.warning( "JSC_SUSPICIOUS_INSTANCEOF_LEFT", "\"instanceof\" with left non-object operand is always false."); @Override public void visit(NodeTraversal t, Node n, Node parent) { checkMissingSemicolon(t, n); checkNaN(t, n); checkInvalidIn(t, n); checkNonObjectInstanceOf(t, n); } private void checkMissingSemicolon(NodeTraversal t, Node n) { switch (n.getType()) { case Token.IF: Node trueCase = n.getSecondChild(); reportIfWasEmpty(t, trueCase); Node elseCase = trueCase.getNext(); if (elseCase != null) { reportIfWasEmpty(t, elseCase); } break; case Token.WHILE: case Token.FOR: case Token.FOR_OF: reportIfWasEmpty(t, NodeUtil.getLoopCodeBlock(n)); break; } } private static void reportIfWasEmpty(NodeTraversal t, Node block) { Preconditions.checkState(block.isBlock()); // A semicolon is distinguished from a block without children by // annotating it with EMPTY_BLOCK. Blocks without children are // usually intentional, especially with loops. if (!block.hasChildren() && block.isAddedBlock()) { t.getCompiler().report(t.makeError(block, SUSPICIOUS_SEMICOLON)); } } private void checkNaN(NodeTraversal t, Node n) { switch (n.getType()) { case Token.EQ: case Token.GE: case Token.GT: case Token.LE: case Token.LT: case Token.NE: case Token.SHEQ: case Token.SHNE: reportIfNaN(t, n.getFirstChild()); reportIfNaN(t, n.getLastChild()); } } private static void reportIfNaN(NodeTraversal t, Node n) { if (NodeUtil.isNaN(n)) { t.getCompiler().report(t.makeError(n.getParent(), SUSPICIOUS_COMPARISON_WITH_NAN)); } } private void checkInvalidIn(NodeTraversal t, Node n) { if (n.getType() == Token.IN) { reportIfNonObject(t, n.getLastChild(), SUSPICIOUS_IN_OPERATOR); } } private void checkNonObjectInstanceOf(NodeTraversal t, Node n) { if (n.getType() == Token.INSTANCEOF) { reportIfNonObject(t, n.getFirstChild(), SUSPICIOUS_INSTANCEOF_LEFT_OPERAND); } } private static boolean reportIfNonObject(NodeTraversal t, Node n, DiagnosticType diagnosticType) { if (n.isAdd() || !NodeUtil.mayBeObect(n)) { t.report(n.getParent(), diagnosticType); return true; } return false; } }
/** * DisambiguateProperties renames properties to disambiguate between unrelated fields with the same * name. Two properties are considered related if they share a definition on their prototype chains, * or if they are potentially referenced together via union types. * * <p>Renamimg only occurs if there are two or more distinct properties with the same name. * * <p>This pass allows other passes, such as inlining and code removal to take advantage of type * information implicitly. * * <pre> * Foo.a; * Bar.a; * </pre> * * <p>will become * * <pre> * Foo.a$Foo; * Bar.a$Bar; * </pre> */ class DisambiguateProperties<T> implements CompilerPass { private static final Logger logger = Logger.getLogger(DisambiguateProperties.class.getName()); // TODO(user): add a flag to allow enabling of this once apps start // using it. static final DiagnosticType INVALIDATION = DiagnosticType.warning( "JSC_INVALIDATION", "Property disambiguator skipping all instances of property {0} " + "because of type {1} node {2}"); private final boolean showInvalidationWarnings = false; private final AbstractCompiler compiler; private final TypeSystem<T> typeSystem; private class Property { /** The name of the property. */ final String name; /** All types on which the field exists, grouped together if related. */ private UnionFind<T> types; /** * A set of types for which renaming this field should be skipped. This list is first filled by * fields defined in the externs file. */ Set<T> typesToSkip = Sets.newHashSet(); /** * If true, do not rename any instance of this field, as it has been referenced from an unknown * type. */ boolean skipRenaming; /** Set of nodes for this field that need renaming. */ Set<Node> renameNodes = Sets.newHashSet(); /** * Map from node to the highest type in the prototype chain containing the field for that node. * In the case of a union, the type is the highest type of one of the types in the union. */ final Map<Node, T> rootTypes = Maps.newHashMap(); Property(String name) { this.name = name; } /** Returns the types on which this field is referenced. */ UnionFind<T> getTypes() { if (types == null) { types = new StandardUnionFind<T>(); } return types; } /** * Record that this property is referenced from this type. * * @return true if the type was recorded for this property, else false, which would happen if * the type was invalidating. */ boolean addType(T type, T top, T relatedType) { checkState(!skipRenaming, "Attempt to record skipped property: %s", name); if (typeSystem.isInvalidatingType(top)) { invalidate(); return false; } else { if (typeSystem.isTypeToSkip(top)) { addTypeToSkip(top); } if (relatedType == null) { getTypes().add(top); } else { getTypes().union(top, relatedType); } typeSystem.recordInterfaces(type, top, this); return true; } } /** Records the given type as one to skip for this property. */ void addTypeToSkip(T type) { for (T skipType : typeSystem.getTypesToSkipForType(type)) { typesToSkip.add(skipType); getTypes().union(skipType, type); } } /** Invalidates any types related to invalid types. */ void expandTypesToSkip() { // If we are not going to rename any properties, then we do not need to // update the list of invalid types, as they are all invalid. if (shouldRename()) { int count = 0; while (true) { // It should usually only take one time through this do-while. checkState(++count < 10, "Stuck in loop expanding types to skip."); // Make sure that the representative type for each type to skip is // marked as being skipped. Set<T> rootTypesToSkip = Sets.newHashSet(); for (T subType : typesToSkip) { rootTypesToSkip.add(types.find(subType)); } typesToSkip.addAll(rootTypesToSkip); Set<T> newTypesToSkip = Sets.newHashSet(); Set<T> allTypes = types.elements(); int originalTypesSize = allTypes.size(); for (T subType : allTypes) { if (!typesToSkip.contains(subType) && typesToSkip.contains(types.find(subType))) { newTypesToSkip.add(subType); } } for (T newType : newTypesToSkip) { addTypeToSkip(newType); } // If there were not any new types added, we are done here. if (types.elements().size() == originalTypesSize) { break; } } } } /** Returns true if any instance of this property should be renamed. */ boolean shouldRename() { return !skipRenaming && types != null && types.allEquivalenceClasses().size() > 1; } /** * Returns true if this property should be renamed on this type. expandTypesToSkip() should be * called before this, if anything has been added to the typesToSkip list. */ boolean shouldRename(T type) { return !skipRenaming && !typesToSkip.contains(type); } /** * Invalidates a field from renaming. Used for field references on an object with unknown type. */ boolean invalidate() { boolean changed = !skipRenaming; skipRenaming = true; types = null; return changed; } /** * Schedule the node to potentially be renamed. * * @param node the node to rename * @param type the highest type in the prototype chain for which the property is defined * @return True if type was accepted without invalidation or if the property was already * invalidated. False if this property was invalidated this time. */ boolean scheduleRenaming(Node node, T type) { if (!skipRenaming) { if (typeSystem.isInvalidatingType(type)) { invalidate(); return false; } renameNodes.add(node); rootTypes.put(node, type); } return true; } } private Map<String, Property> properties = Maps.newHashMap(); static DisambiguateProperties<JSType> forJSTypeSystem(AbstractCompiler compiler) { return new DisambiguateProperties<JSType>(compiler, new JSTypeSystem(compiler)); } static DisambiguateProperties<ConcreteType> forConcreteTypeSystem( AbstractCompiler compiler, TightenTypes tt) { return new DisambiguateProperties<ConcreteType>( compiler, new ConcreteTypeSystem(tt, compiler.getCodingConvention())); } /** * This constructor should only be called by one of the helper functions above for either the * JSType system, or the concrete type system. */ private DisambiguateProperties(AbstractCompiler compiler, TypeSystem<T> typeSystem) { this.compiler = compiler; this.typeSystem = typeSystem; } public void process(Node externs, Node root) { for (TypeMismatch mis : compiler.getTypeValidator().getMismatches()) { addInvalidatingType(mis.typeA); addInvalidatingType(mis.typeB); } StaticScope<T> scope = typeSystem.getRootScope(); NodeTraversal.traverse(compiler, externs, new FindExternProperties()); NodeTraversal.traverse(compiler, root, new FindRenameableProperties()); renameProperties(); } /** Invalidates the given type, so that no properties on it will be renamed. */ private void addInvalidatingType(JSType type) { type = type.restrictByNotNullOrUndefined(); if (type instanceof UnionType) { for (JSType alt : ((UnionType) type).getAlternates()) { addInvalidatingType(alt); } return; } typeSystem.addInvalidatingType(type); ObjectType objType = ObjectType.cast(type); if (objType != null && objType.getImplicitPrototype() != null) { typeSystem.addInvalidatingType(objType.getImplicitPrototype()); } } /** Returns the property for the given name, creating it if necessary. */ protected Property getProperty(String name) { if (!properties.containsKey(name)) { properties.put(name, new Property(name)); } return properties.get(name); } /** Public for testing. */ T getTypeWithProperty(String field, T type) { return typeSystem.getTypeWithProperty(field, type); } /** Tracks the current type system scope while traversing. */ private abstract class AbstractScopingCallback implements ScopedCallback { protected final Stack<StaticScope<T>> scopes = new Stack<StaticScope<T>>(); public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } public void enterScope(NodeTraversal t) { if (t.inGlobalScope()) { scopes.push(typeSystem.getRootScope()); } else { scopes.push(typeSystem.getFunctionScope(t.getScopeRoot())); } } public void exitScope(NodeTraversal t) { scopes.pop(); } /** Returns the current scope at this point in the file. */ protected StaticScope<T> getScope() { return scopes.peek(); } } /** * Finds all properties defined in the externs file and sets them as ineligible for renaming from * the type on which they are defined. */ private class FindExternProperties extends AbstractScopingCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { // TODO(johnlenz): Support object-literal property definitions. if (n.getType() == Token.GETPROP) { String field = n.getLastChild().getString(); T type = typeSystem.getType(getScope(), n.getFirstChild(), field); Property prop = getProperty(field); if (typeSystem.isInvalidatingType(type)) { prop.invalidate(); } else { prop.addTypeToSkip(type); // If this is a prototype property, then we want to skip assignments // to the instance type as well. These assignments are not usually // seen in the extern code itself, so we must handle them here. if ((type = typeSystem.getInstanceFromPrototype(type)) != null) { prop.getTypes().add(type); prop.typesToSkip.add(type); } } } } } /** * Traverses the tree, building a map from field names to Nodes for all fields that can be * renamed. */ private class FindRenameableProperties extends AbstractScopingCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.getType() == Token.GETPROP) { handleGetProp(t, n); } else if (n.getType() == Token.OBJECTLIT) { handleObjectLit(t, n); } } /** Processes a GETPROP node. */ private void handleGetProp(NodeTraversal t, Node n) { String name = n.getLastChild().getString(); T type = typeSystem.getType(getScope(), n.getFirstChild(), name); Property prop = getProperty(name); if (!prop.scheduleRenaming(n.getLastChild(), processProperty(t, prop, type, null))) { if (showInvalidationWarnings) { compiler.report( JSError.make( t.getSourceName(), n, INVALIDATION, name, (type == null ? "null" : type.toString()), n.toString())); } } } /** Processes a OBJECTLIT node. */ private void handleObjectLit(NodeTraversal t, Node n) { Node child = n.getFirstChild(); while (child != null) { // Maybe STRING, NUMBER, GET, SET if (child.getType() != Token.NUMBER) { // We should never see a mix of numbers and strings. String name = child.getString(); T type = typeSystem.getType(getScope(), n, name); Property prop = getProperty(name); if (!prop.scheduleRenaming(child, processProperty(t, prop, type, null))) { if (showInvalidationWarnings) { compiler.report( JSError.make( t.getSourceName(), child, INVALIDATION, name, (type == null ? "null" : type.toString()), n.toString())); } } } child = child.getNext(); } } /** * Processes a property, adding it to the list of properties to rename. * * @return a representative type for the property reference, which will be the highest type on * the prototype chain of the provided type. In the case of a union type, it will be the * highest type on the prototype chain of one of the members of the union. */ private T processProperty(NodeTraversal t, Property prop, T type, T relatedType) { type = typeSystem.restrictByNotNullOrUndefined(type); if (prop.skipRenaming || typeSystem.isInvalidatingType(type)) { return null; } Iterable<T> alternatives = typeSystem.getTypeAlternatives(type); if (alternatives != null) { T firstType = relatedType; for (T subType : alternatives) { T lastType = processProperty(t, prop, subType, firstType); if (lastType != null) { firstType = firstType == null ? lastType : firstType; } } return firstType; } else { T topType = typeSystem.getTypeWithProperty(prop.name, type); if (typeSystem.isInvalidatingType(topType)) { return null; } prop.addType(type, topType, relatedType); return topType; } } } /** Renames all properties with references on more than one type. */ void renameProperties() { int propsRenamed = 0, propsSkipped = 0, instancesRenamed = 0, instancesSkipped = 0, singleTypeProps = 0; for (Property prop : properties.values()) { if (prop.shouldRename()) { Map<T, String> propNames = buildPropNames(prop.getTypes(), prop.name); ++propsRenamed; prop.expandTypesToSkip(); UnionFind<T> types = prop.getTypes(); for (Node node : prop.renameNodes) { T rootType = prop.rootTypes.get(node); if (prop.shouldRename(rootType)) { String newName = propNames.get(rootType); node.setString(newName); compiler.reportCodeChange(); ++instancesRenamed; } else { ++instancesSkipped; } } } else { if (prop.skipRenaming) { ++propsSkipped; } else { ++singleTypeProps; } } } logger.info("Renamed " + instancesRenamed + " instances of " + propsRenamed + " properties."); logger.info( "Skipped renaming " + instancesSkipped + " invalidated " + "properties, " + propsSkipped + " instances of properties " + "that were skipped for specific types and " + singleTypeProps + " properties that were referenced from only one type."); } /** * Chooses a name to use for renaming in each equivalence class and maps each type in that class * to it. */ private Map<T, String> buildPropNames(UnionFind<T> types, String name) { Map<T, String> names = Maps.newHashMap(); for (Set<T> set : types.allEquivalenceClasses()) { checkState(!set.isEmpty()); String typeName = null; for (T type : set) { if (typeName == null || type.toString().compareTo(typeName) < 0) { typeName = type.toString(); } } String newName; if ("{...}".equals(typeName)) { newName = name; } else { newName = typeName.replaceAll("[^\\w$]", "_") + "$" + name; } for (T type : set) { names.put(type, newName); } } return names; } /** Returns a map from field name to types for which it will be renamed. */ Multimap<String, Collection<T>> getRenamedTypesForTesting() { Multimap<String, Collection<T>> ret = HashMultimap.create(); for (Map.Entry<String, Property> entry : properties.entrySet()) { Property prop = entry.getValue(); if (!prop.skipRenaming) { for (Collection<T> c : prop.getTypes().allEquivalenceClasses()) { if (!c.isEmpty() && !prop.typesToSkip.contains(c.iterator().next())) { ret.put(entry.getKey(), c); } } } } return ret; } /** Interface for providing the type information needed by this pass. */ private interface TypeSystem<T> { // TODO(user): add a getUniqueName(T type) method that is guaranteed // to be unique, performant and human-readable. /** Returns the top-most scope used by the type system (if any). */ StaticScope<T> getRootScope(); /** Returns the new scope started at the given function node. */ StaticScope<T> getFunctionScope(Node node); /** * Returns the type of the given node. * * @param prop Only types with this property need to be returned. In general with type * tightening, this will require no special processing, but in the case of an unknown * JSType, we might need to add in the native types since we don't track them, but only if * they have the given property. */ T getType(StaticScope<T> scope, Node node, String prop); /** * Returns true if a field reference on this type will invalidiate all references to that field * as candidates for renaming. This is true if the type is unknown or all-inclusive, as * variables with such a type could be references to any object. */ boolean isInvalidatingType(T type); /** * Informs the given type system that a type is invalidating due to a type mismatch found during * type checking. */ void addInvalidatingType(JSType type); /** * Returns a set of types that should be skipped given the given type. This is necessary for * interfaces when using JSTypes, as all super interfaces must also be skipped. */ ImmutableSet<T> getTypesToSkipForType(T type); /** * Determines whether the given type is one whose properties should not be considered for * renaming. */ boolean isTypeToSkip(T type); /** Remove null and undefined from the options in the given type. */ T restrictByNotNullOrUndefined(T type); /** * Returns the alternatives if this is a type that represents multiple types, and null if not. * Union and interface types can correspond to multiple other types. */ Iterable<T> getTypeAlternatives(T type); /** * Returns the type in the chain from the given type that contains the given field or null if it * is not found anywhere. */ T getTypeWithProperty(String field, T type); /** * Returns the type of the instance of which this is the prototype or null if this is not a * function prototype. */ T getInstanceFromPrototype(T type); /** * Records that this property could be referenced from any interface that this type, or any type * in its superclass chain, implements. */ void recordInterfaces(T type, T relatedType, DisambiguateProperties<T>.Property p); } /** Implementation of TypeSystem using JSTypes. */ private static class JSTypeSystem implements TypeSystem<JSType> { private final Set<JSType> invalidatingTypes; private JSTypeRegistry registry; public JSTypeSystem(AbstractCompiler compiler) { registry = compiler.getTypeRegistry(); invalidatingTypes = Sets.newHashSet( registry.getNativeType(JSTypeNative.ALL_TYPE), registry.getNativeType(JSTypeNative.NO_OBJECT_TYPE), registry.getNativeType(JSTypeNative.NO_TYPE), registry.getNativeType(JSTypeNative.FUNCTION_PROTOTYPE), registry.getNativeType(JSTypeNative.FUNCTION_INSTANCE_TYPE), registry.getNativeType(JSTypeNative.OBJECT_PROTOTYPE), registry.getNativeType(JSTypeNative.TOP_LEVEL_PROTOTYPE), registry.getNativeType(JSTypeNative.UNKNOWN_TYPE)); } @Override public void addInvalidatingType(JSType type) { checkState(!type.isUnionType()); invalidatingTypes.add(type); } @Override public StaticScope<JSType> getRootScope() { return null; } @Override public StaticScope<JSType> getFunctionScope(Node node) { return null; } @Override public JSType getType(StaticScope<JSType> scope, Node node, String prop) { if (node.getJSType() == null) { return registry.getNativeType(JSTypeNative.UNKNOWN_TYPE); } return node.getJSType(); } @Override public boolean isInvalidatingType(JSType type) { if (type == null || invalidatingTypes.contains(type) || type.isUnknownType() /* unresolved types */) { return true; } ObjectType objType = ObjectType.cast(type); return objType != null && !objType.hasReferenceName(); } @Override public ImmutableSet<JSType> getTypesToSkipForType(JSType type) { type = type.restrictByNotNullOrUndefined(); if (type instanceof UnionType) { Set<JSType> types = Sets.newHashSet(type); for (JSType alt : ((UnionType) type).getAlternates()) { types.addAll(getTypesToSkipForTypeNonUnion(type)); } return ImmutableSet.copyOf(types); } return ImmutableSet.copyOf(getTypesToSkipForTypeNonUnion(type)); } private Set<JSType> getTypesToSkipForTypeNonUnion(JSType type) { Set<JSType> types = Sets.newHashSet(); JSType skipType = type; while (skipType != null) { types.add(skipType); ObjectType objSkipType = skipType.toObjectType(); if (objSkipType != null) { skipType = objSkipType.getImplicitPrototype(); } else { break; } } return types; } @Override public boolean isTypeToSkip(JSType type) { return type.isEnumType() || (type.autoboxesTo() != null); } @Override public JSType restrictByNotNullOrUndefined(JSType type) { return type.restrictByNotNullOrUndefined(); } @Override public Iterable<JSType> getTypeAlternatives(JSType type) { if (type.isUnionType()) { return ((UnionType) type).getAlternates(); } else { ObjectType objType = type.toObjectType(); if (objType != null && objType.getConstructor() != null && objType.getConstructor().isInterface()) { List<JSType> list = Lists.newArrayList(); for (FunctionType impl : registry.getDirectImplementors(objType)) { list.add(impl.getInstanceType()); } return list; } else { return null; } } } @Override public ObjectType getTypeWithProperty(String field, JSType type) { if (!(type instanceof ObjectType)) { if (type.autoboxesTo() != null) { type = type.autoboxesTo(); } else { return null; } } // Ignore the prototype itself at all times. if ("prototype".equals(field)) { return null; } // We look up the prototype chain to find the highest place (if any) that // this appears. This will make references to overriden properties look // like references to the initial property, so they are renamed alike. ObjectType foundType = null; ObjectType objType = ObjectType.cast(type); while (objType != null && objType.getImplicitPrototype() != objType) { if (objType.hasOwnProperty(field)) { foundType = objType; } objType = objType.getImplicitPrototype(); } // If the property does not exist on the referenced type but the original // type is an object type, see if any subtype has the property. if (foundType == null) { ObjectType maybeType = ObjectType.cast(registry.getGreatestSubtypeWithProperty(type, field)); // getGreatestSubtypeWithProperty does not guarantee that the property // is defined on the returned type, it just indicates that it might be, // so we have to double check. if (maybeType != null && maybeType.hasOwnProperty(field)) { foundType = maybeType; } } return foundType; } @Override public JSType getInstanceFromPrototype(JSType type) { if (type.isFunctionPrototypeType()) { FunctionPrototypeType prototype = (FunctionPrototypeType) type; FunctionType owner = prototype.getOwnerFunction(); if (owner.isConstructor() || owner.isInterface()) { return ((FunctionPrototypeType) type).getOwnerFunction().getInstanceType(); } } return null; } @Override public void recordInterfaces( JSType type, JSType relatedType, DisambiguateProperties<JSType>.Property p) { ObjectType objType = ObjectType.cast(type); if (objType != null) { FunctionType constructor; if (objType instanceof FunctionType) { constructor = (FunctionType) objType; } else if (objType instanceof FunctionPrototypeType) { constructor = ((FunctionPrototypeType) objType).getOwnerFunction(); } else { constructor = objType.getConstructor(); } while (constructor != null) { for (ObjectType itype : constructor.getImplementedInterfaces()) { JSType top = getTypeWithProperty(p.name, itype); if (top != null) { p.addType(itype, top, relatedType); } else { recordInterfaces(itype, relatedType, p); } // If this interface invalidated this property, return now. if (p.skipRenaming) return; } if (constructor.isInterface() || constructor.isConstructor()) { constructor = constructor.getSuperClassConstructor(); } else { constructor = null; } } } } } /** Implementation of TypeSystem using concrete types. */ private static class ConcreteTypeSystem implements TypeSystem<ConcreteType> { private final TightenTypes tt; private int nextUniqueId; private CodingConvention codingConvention; private final Set<JSType> invalidatingTypes = Sets.newHashSet(); // An array of native types that are not tracked by type tightening, and // thus need to be added in if an unknown type is encountered. private static final JSTypeNative[] nativeTypes = new JSTypeNative[] { JSTypeNative.BOOLEAN_OBJECT_TYPE, JSTypeNative.NUMBER_OBJECT_TYPE, JSTypeNative.STRING_OBJECT_TYPE }; public ConcreteTypeSystem(TightenTypes tt, CodingConvention convention) { this.tt = tt; this.codingConvention = convention; } @Override public void addInvalidatingType(JSType type) { checkState(!type.isUnionType()); invalidatingTypes.add(type); } @Override public StaticScope<ConcreteType> getRootScope() { return tt.getTopScope(); } @Override public StaticScope<ConcreteType> getFunctionScope(Node decl) { ConcreteFunctionType func = tt.getConcreteFunction(decl); return (func != null) ? func.getScope() : (StaticScope<ConcreteType>) null; } @Override public ConcreteType getType(StaticScope<ConcreteType> scope, Node node, String prop) { if (scope != null) { ConcreteType c = tt.inferConcreteType((TightenTypes.ConcreteScope) scope, node); return maybeAddAutoboxes(c, node, prop); } else { return null; } } /** * Add concrete types for autoboxing types if necessary. The concrete type system does not track * native types, like string, so add them if they are present in the JSType for the node. */ private ConcreteType maybeAddAutoboxes(ConcreteType cType, Node node, String prop) { JSType jsType = node.getJSType(); if (jsType == null) { return cType; } else if (jsType.isUnknownType()) { for (JSTypeNative nativeType : nativeTypes) { ConcreteType concrete = tt.getConcreteInstance(tt.getTypeRegistry().getNativeObjectType(nativeType)); if (concrete != null && !concrete.getPropertyType(prop).isNone()) { cType = cType.unionWith(concrete); } } return cType; } return maybeAddAutoboxes(cType, jsType, prop); } private ConcreteType maybeAddAutoboxes(ConcreteType cType, JSType jsType, String prop) { jsType = jsType.restrictByNotNullOrUndefined(); if (jsType instanceof UnionType) { for (JSType alt : ((UnionType) jsType).getAlternates()) { return maybeAddAutoboxes(cType, alt, prop); } } if (jsType.autoboxesTo() != null) { JSType autoboxed = jsType.autoboxesTo(); return cType.unionWith(tt.getConcreteInstance((ObjectType) autoboxed)); } else if (jsType.unboxesTo() != null) { return cType.unionWith(tt.getConcreteInstance((ObjectType) jsType)); } return cType; } @Override public boolean isInvalidatingType(ConcreteType type) { // We will disallow types on functions so that 'prototype' is not renamed. // TODO(user): Support properties on functions as well. return (type == null) || type.isAll() || type.isFunction() || (type.isInstance() && invalidatingTypes.contains(type.toInstance().instanceType)); } @Override public ImmutableSet<ConcreteType> getTypesToSkipForType(ConcreteType type) { return ImmutableSet.of(type); } @Override public boolean isTypeToSkip(ConcreteType type) { // Skip anonymous object literals and enum types. return type.isInstance() && !(type.toInstance().isFunctionPrototype() || type.toInstance().instanceType.isInstanceType()); } @Override public ConcreteType restrictByNotNullOrUndefined(ConcreteType type) { // These are not represented in concrete types. return type; } @Override public Iterable<ConcreteType> getTypeAlternatives(ConcreteType type) { if (type.isUnion()) { return ((ConcreteUnionType) type).getAlternatives(); } else { return null; } } @Override public ConcreteType getTypeWithProperty(String field, ConcreteType type) { if (type.isInstance()) { ConcreteInstanceType instanceType = (ConcreteInstanceType) type; return instanceType.getInstanceTypeWithProperty(field); } else if (type.isFunction()) { if ("prototype".equals(field) || codingConvention.isSuperClassReference(field)) { return type; } } else if (type.isNone()) { // If the receiver is none, then this code is never reached. We will // return a new fake type to ensure that this access is renamed // differently from any other, so it can be easily removed. return new ConcreteUniqueType(++nextUniqueId); } else if (type.isUnion()) { // If only one has the property, return that. for (ConcreteType t : ((ConcreteUnionType) type).getAlternatives()) { ConcreteType ret = getTypeWithProperty(field, t); if (ret != null) { return ret; } } } return null; } @Override public ConcreteType getInstanceFromPrototype(ConcreteType type) { if (type.isInstance()) { ConcreteInstanceType instanceType = (ConcreteInstanceType) type; if (instanceType.isFunctionPrototype()) { return instanceType.getConstructorType().getInstanceType(); } } return null; } @Override public void recordInterfaces( ConcreteType type, ConcreteType relatedType, DisambiguateProperties<ConcreteType>.Property p) { // No need to record interfaces when using concrete types. } } }
/** * 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(); } }
/** A compiler pass to run the type inference analysis. */ class TypeInferencePass implements CompilerPass { static final DiagnosticType DATAFLOW_ERROR = DiagnosticType.warning("JSC_INTERNAL_ERROR_DATAFLOW", "non-monotonic data-flow analysis"); private final AbstractCompiler compiler; private final ReverseAbstractInterpreter reverseInterpreter; private final Scope topScope; private final MemoizedScopeCreator scopeCreator; private final Map<String, AssertionFunctionSpec> assertionFunctionsMap; TypeInferencePass( AbstractCompiler compiler, ReverseAbstractInterpreter reverseInterpreter, Scope topScope, MemoizedScopeCreator scopeCreator) { this.compiler = compiler; this.reverseInterpreter = reverseInterpreter; this.topScope = topScope; this.scopeCreator = scopeCreator; assertionFunctionsMap = Maps.newHashMap(); for (AssertionFunctionSpec assertionFunction : compiler.getCodingConvention().getAssertionFunctions()) { assertionFunctionsMap.put(assertionFunction.getFunctionName(), assertionFunction); } } /** * Main entry point for type inference when running over the whole tree. * * @param externsRoot The root of the externs parse tree. * @param jsRoot The root of the input parse tree to be checked. */ @Override public void process(Node externsRoot, Node jsRoot) { Node externsAndJs = jsRoot.getParent(); Preconditions.checkState(externsAndJs != null); Preconditions.checkState(externsRoot == null || externsAndJs.hasChild(externsRoot)); inferAllScopes(externsAndJs); } /** Entry point for type inference when running over part of the tree. */ void inferAllScopes(Node node) { // Type analysis happens in two major phases. // 1) Finding all the symbols. // 2) Propagating all the inferred types. // // The order of this analysis is non-obvious. In a complete inference // system, we may need to backtrack arbitrarily far. But the compile-time // costs would be unacceptable. // // We do one pass where we do typed scope creation for all scopes // in pre-order. // // Then we do a second pass where we do all type inference // (type propagation) in pre-order. // // We use a memoized scope creator so that we never create a scope // more than once. // // This will allow us to handle cases like: // var ns = {}; // (function() { /** JSDoc */ ns.method = function() {}; })(); // ns.method(); // In this code, we need to build the symbol table for the inner scope in // order to propagate the type of ns.method in the outer scope. (new NodeTraversal(compiler, new FirstScopeBuildingCallback(), scopeCreator)) .traverseWithScope(node, topScope); for (Scope s : scopeCreator.getAllMemoizedScopes()) { s.resolveTypes(); } (new NodeTraversal(compiler, new SecondScopeBuildingCallback(), scopeCreator)) .traverseWithScope(node, topScope); } void inferScope(Node n, Scope scope) { TypeInference typeInference = new TypeInference( compiler, computeCfg(n), reverseInterpreter, scope, assertionFunctionsMap); try { typeInference.analyze(); // Resolve any new type names found during the inference. compiler.getTypeRegistry().resolveTypesInScope(scope); } catch (DataFlowAnalysis.MaxIterationsExceededException e) { compiler.report(JSError.make(n, DATAFLOW_ERROR)); } } private static class FirstScopeBuildingCallback extends AbstractScopedCallback { @Override public void enterScope(NodeTraversal t) { t.getScope(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { // Do nothing } } private class SecondScopeBuildingCallback extends AbstractScopedCallback { @Override public void enterScope(NodeTraversal t) { // Only infer the entry root, rather than the scope root. // This ensures that incremental compilation only touches the root // that's been swapped out. inferScope(t.getCurrentNode(), t.getScope()); } @Override public void visit(NodeTraversal t, Node n, Node parent) { // Do nothing } } private ControlFlowGraph<Node> computeCfg(Node n) { ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, false); cfa.process(null, n); return cfa.getCfg(); } }
/** * Named groups of DiagnosticTypes exposed by Compiler. * * @author [email protected] (Nick Santos) */ public class DiagnosticGroups { static final DiagnosticType UNUSED = DiagnosticType.warning("JSC_UNUSED", "{0}"); public static final Set<String> wildcardExcludedGroups = ImmutableSet.of("reportUnknownTypes"); public DiagnosticGroups() {} private static final Map<String, DiagnosticGroup> groupsByName = new HashMap<>(); static DiagnosticGroup registerDeprecatedGroup(String name) { return registerGroup(name, new DiagnosticGroup(name, UNUSED)); } static DiagnosticGroup registerGroup(String name, DiagnosticGroup group) { groupsByName.put(name, group); return group; } static DiagnosticGroup registerGroup(String name, DiagnosticType... types) { DiagnosticGroup group = new DiagnosticGroup(name, types); groupsByName.put(name, group); return group; } static DiagnosticGroup registerGroup(String name, DiagnosticGroup... groups) { DiagnosticGroup group = new DiagnosticGroup(name, groups); groupsByName.put(name, group); return group; } /** Get the registered diagnostic groups, indexed by name. */ protected Map<String, DiagnosticGroup> getRegisteredGroups() { return ImmutableMap.copyOf(groupsByName); } /** Find the diagnostic group registered under the given name. */ public DiagnosticGroup forName(String name) { return groupsByName.get(name); } // A bit of a hack to display the available groups on the command-line. // New groups should be added to this list if they are public and should // be listed on the command-line as an available option. // // If a group is suppressible on a per-file basis, it should be added // to parser/ParserConfig.properties static final String DIAGNOSTIC_GROUP_NAMES = "accessControls, ambiguousFunctionDecl, checkEventfulObjectDisposal, " + "checkRegExp, checkTypes, checkVars, " + "conformanceViolations, const, constantProperty, deprecated, " + "deprecatedAnnotations, duplicateMessage, es3, " + "es5Strict, externsValidation, fileoverviewTags, globalThis, " + "inferredConstCheck, internetExplorerChecks, invalidCasts, " + "misplacedTypeAnnotation, missingGetCssName, missingProperties, " + "missingProvide, missingRequire, missingReturn, msgDescriptions" + "newCheckTypes, nonStandardJsDocs, reportUnknownTypes, suspiciousCode, " + "strictModuleDepCheck, typeInvalidation, " + "undefinedNames, undefinedVars, unknownDefines, unnecessaryCasts, uselessCode, " + "useOfGoogBase, visibility"; public static final DiagnosticGroup GLOBAL_THIS = DiagnosticGroups.registerGroup("globalThis", CheckGlobalThis.GLOBAL_THIS); public static final DiagnosticGroup DEPRECATED = DiagnosticGroups.registerGroup( "deprecated", CheckAccessControls.DEPRECATED_NAME, CheckAccessControls.DEPRECATED_NAME_REASON, CheckAccessControls.DEPRECATED_PROP, CheckAccessControls.DEPRECATED_PROP_REASON, CheckAccessControls.DEPRECATED_CLASS, CheckAccessControls.DEPRECATED_CLASS_REASON); public static final DiagnosticGroup VISIBILITY = DiagnosticGroups.registerGroup( "visibility", CheckAccessControls.BAD_PRIVATE_GLOBAL_ACCESS, CheckAccessControls.BAD_PRIVATE_PROPERTY_ACCESS, CheckAccessControls.BAD_PACKAGE_PROPERTY_ACCESS, CheckAccessControls.BAD_PROTECTED_PROPERTY_ACCESS, CheckAccessControls.EXTEND_FINAL_CLASS, CheckAccessControls.PRIVATE_OVERRIDE, CheckAccessControls.VISIBILITY_MISMATCH, CheckAccessControls.CONVENTION_MISMATCH); public static final DiagnosticGroup ACCESS_CONTROLS = DiagnosticGroups.registerGroup("accessControls", DEPRECATED, VISIBILITY); public static final DiagnosticGroup NON_STANDARD_JSDOC = DiagnosticGroups.registerGroup( "nonStandardJsDocs", RhinoErrorReporter.BAD_JSDOC_ANNOTATION, RhinoErrorReporter.INVALID_PARAM, RhinoErrorReporter.JSDOC_IN_BLOCK_COMMENT); public static final DiagnosticGroup INVALID_CASTS = DiagnosticGroups.registerGroup("invalidCasts", TypeValidator.INVALID_CAST); public static final DiagnosticGroup UNNECESSARY_CASTS = DiagnosticGroups.registerGroup("unnecessaryCasts", TypeValidator.UNNECESSARY_CAST); public static final DiagnosticGroup INFERRED_CONST_CHECKS = DiagnosticGroups.registerGroup( "inferredConstCheck", TypedScopeCreator.CANNOT_INFER_CONST_TYPE); public static final DiagnosticGroup FILEOVERVIEW_JSDOC = DiagnosticGroups.registerDeprecatedGroup("fileoverviewTags"); public static final DiagnosticGroup STRICT_MODULE_DEP_CHECK = DiagnosticGroups.registerGroup( "strictModuleDepCheck", VarCheck.STRICT_MODULE_DEP_ERROR, CheckGlobalNames.STRICT_MODULE_DEP_QNAME); public static final DiagnosticGroup VIOLATED_MODULE_DEP = DiagnosticGroups.registerGroup("violatedModuleDep", VarCheck.VIOLATED_MODULE_DEP_ERROR); public static final DiagnosticGroup EXTERNS_VALIDATION = DiagnosticGroups.registerGroup( "externsValidation", VarCheck.NAME_REFERENCE_IN_EXTERNS_ERROR, VarCheck.UNDEFINED_EXTERN_VAR_ERROR); public static final DiagnosticGroup AMBIGUOUS_FUNCTION_DECL = DiagnosticGroups.registerGroup( "ambiguousFunctionDecl", VariableReferenceCheck.AMBIGUOUS_FUNCTION_DECL, StrictModeCheck.BAD_FUNCTION_DECLARATION); public static final DiagnosticGroup UNKNOWN_DEFINES = DiagnosticGroups.registerGroup("unknownDefines", ProcessDefines.UNKNOWN_DEFINE_WARNING); public static final DiagnosticGroup TWEAKS = DiagnosticGroups.registerGroup( "tweakValidation", ProcessTweaks.INVALID_TWEAK_DEFAULT_VALUE_WARNING, ProcessTweaks.TWEAK_WRONG_GETTER_TYPE_WARNING, ProcessTweaks.UNKNOWN_TWEAK_WARNING); public static final DiagnosticGroup MISSING_PROPERTIES = DiagnosticGroups.registerGroup( "missingProperties", TypeCheck.INEXISTENT_PROPERTY, TypeCheck.INEXISTENT_PROPERTY_WITH_SUGGESTION, TypeCheck.POSSIBLE_INEXISTENT_PROPERTY); public static final DiagnosticGroup MISSING_RETURN = DiagnosticGroups.registerGroup("missingReturn", CheckMissingReturn.MISSING_RETURN_STATEMENT); public static final DiagnosticGroup INTERNET_EXPLORER_CHECKS = DiagnosticGroups.registerGroup("internetExplorerChecks", RhinoErrorReporter.TRAILING_COMMA); public static final DiagnosticGroup UNDEFINED_VARIABLES = DiagnosticGroups.registerGroup("undefinedVars", VarCheck.UNDEFINED_VAR_ERROR); public static final DiagnosticGroup UNDEFINED_NAMES = DiagnosticGroups.registerGroup("undefinedNames", CheckGlobalNames.UNDEFINED_NAME_WARNING); public static final DiagnosticGroup DEBUGGER_STATEMENT_PRESENT = DiagnosticGroups.registerGroup( "checkDebuggerStatement", CheckDebuggerStatement.DEBUGGER_STATEMENT_PRESENT); public static final DiagnosticGroup CHECK_REGEXP = DiagnosticGroups.registerGroup( "checkRegExp", CheckRegExp.REGEXP_REFERENCE, CheckRegExp.MALFORMED_REGEXP); public static final DiagnosticGroup CHECK_TYPES = DiagnosticGroups.registerGroup( "checkTypes", TypeValidator.ALL_DIAGNOSTICS, TypeCheck.ALL_DIAGNOSTICS); // Part of the new type inference (under development) public static final DiagnosticGroup NEW_CHECK_TYPES = DiagnosticGroups.registerGroup( "newCheckTypes", GlobalTypeInfo.ALL_DIAGNOSTICS, NewTypeInference.ALL_DIAGNOSTICS); public static final DiagnosticGroup NEW_CHECK_TYPES_ALL_CHECKS = DiagnosticGroups.registerGroup( "newCheckTypesAllChecks", JSTypeCreatorFromJSDoc.CONFLICTING_SHAPE_TYPE, NewTypeInference.NULLABLE_DEREFERENCE); static { // Warnings that are absent in closure library DiagnosticGroups.registerGroup( "newCheckTypesClosureClean", // JSTypeCreatorFromJSDoc.BAD_JSDOC_ANNOTATION, JSTypeCreatorFromJSDoc.CONFLICTING_EXTENDED_TYPE, JSTypeCreatorFromJSDoc.CONFLICTING_IMPLEMENTED_TYPE, JSTypeCreatorFromJSDoc.DICT_IMPLEMENTS_INTERF, JSTypeCreatorFromJSDoc.EXTENDS_NON_OBJECT, JSTypeCreatorFromJSDoc.EXTENDS_NOT_ON_CTOR_OR_INTERF, JSTypeCreatorFromJSDoc.IMPLEMENTS_WITHOUT_CONSTRUCTOR, JSTypeCreatorFromJSDoc.INHERITANCE_CYCLE, // JSTypeCreatorFromJSDoc.UNION_IS_UNINHABITABLE, GlobalTypeInfo.ANONYMOUS_NOMINAL_TYPE, GlobalTypeInfo.CANNOT_INIT_TYPEDEF, GlobalTypeInfo.CANNOT_OVERRIDE_FINAL_METHOD, GlobalTypeInfo.CONST_WITHOUT_INITIALIZER, // GlobalTypeInfo.COULD_NOT_INFER_CONST_TYPE, GlobalTypeInfo.CTOR_IN_DIFFERENT_SCOPE, GlobalTypeInfo.DUPLICATE_JSDOC, GlobalTypeInfo.DUPLICATE_PROP_IN_ENUM, GlobalTypeInfo.EXPECTED_CONSTRUCTOR, GlobalTypeInfo.EXPECTED_INTERFACE, GlobalTypeInfo.INEXISTENT_PARAM, // GlobalTypeInfo.INVALID_PROP_OVERRIDE, GlobalTypeInfo.LENDS_ON_BAD_TYPE, GlobalTypeInfo.MALFORMED_ENUM, GlobalTypeInfo.MISPLACED_CONST_ANNOTATION, // GlobalTypeInfo.REDECLARED_PROPERTY, GlobalTypeInfo.STRUCTDICT_WITHOUT_CTOR, GlobalTypeInfo.UNDECLARED_NAMESPACE, // GlobalTypeInfo.UNRECOGNIZED_TYPE_NAME, TypeCheck.CONFLICTING_EXTENDED_TYPE, TypeCheck.ENUM_NOT_CONSTANT, TypeCheck.INCOMPATIBLE_EXTENDED_PROPERTY_TYPE, TypeCheck.MULTIPLE_VAR_DEF, TypeCheck.UNKNOWN_OVERRIDE, TypeValidator.INTERFACE_METHOD_NOT_IMPLEMENTED, NewTypeInference.ASSERT_FALSE, NewTypeInference.CANNOT_BIND_CTOR, NewTypeInference.CONST_REASSIGNED, NewTypeInference.CROSS_SCOPE_GOTCHA, // NewTypeInference.FAILED_TO_UNIFY, // NewTypeInference.FORIN_EXPECTS_OBJECT, NewTypeInference.FORIN_EXPECTS_STRING_KEY, // NewTypeInference.GOOG_BIND_EXPECTS_FUNCTION, // NewTypeInference.INVALID_ARGUMENT_TYPE, // NewTypeInference.INVALID_CAST, NewTypeInference.INVALID_INFERRED_RETURN_TYPE, // NewTypeInference.INVALID_OBJLIT_PROPERTY_TYPE, // NewTypeInference.INVALID_OPERAND_TYPE, // NewTypeInference.INVALID_THIS_TYPE_IN_BIND, // NewTypeInference.MISTYPED_ASSIGN_RHS, // NewTypeInference.NON_NUMERIC_ARRAY_INDEX, // NewTypeInference.NOT_A_CONSTRUCTOR, // NewTypeInference.NOT_UNIQUE_INSTANTIATION, // NewTypeInference.POSSIBLY_INEXISTENT_PROPERTY, // NewTypeInference.PROPERTY_ACCESS_ON_NONOBJECT, // NewTypeInference.RETURN_NONDECLARED_TYPE, NewTypeInference.UNKNOWN_ASSERTION_TYPE, // CheckGlobalThis.GLOBAL_THIS, // CheckMissingReturn.MISSING_RETURN_STATEMENT, TypeCheck.CONSTRUCTOR_NOT_CALLABLE, TypeCheck.ILLEGAL_OBJLIT_KEY, // TypeCheck.ILLEGAL_PROPERTY_CREATION, TypeCheck.IN_USED_WITH_STRUCT, // TypeCheck.INEXISTENT_PROPERTY, TypeCheck.NOT_CALLABLE, // TypeCheck.WRONG_ARGUMENT_COUNT, // TypeValidator.ILLEGAL_PROPERTY_ACCESS, TypeValidator.UNKNOWN_TYPEOF_VALUE); } public static final DiagnosticGroup CHECK_EVENTFUL_OBJECT_DISPOSAL = DiagnosticGroups.registerGroup( "checkEventfulObjectDisposal", CheckEventfulObjectDisposal.EVENTFUL_OBJECT_NOT_DISPOSED, CheckEventfulObjectDisposal.EVENTFUL_OBJECT_PURELY_LOCAL, CheckEventfulObjectDisposal.OVERWRITE_PRIVATE_EVENTFUL_OBJECT, CheckEventfulObjectDisposal.UNLISTEN_WITH_ANONBOUND); public static final DiagnosticGroup REPORT_UNKNOWN_TYPES = DiagnosticGroups.registerGroup("reportUnknownTypes", TypeCheck.UNKNOWN_EXPR_TYPE); public static final DiagnosticGroup CHECK_STRUCT_DICT_INHERITANCE = DiagnosticGroups.registerDeprecatedGroup("checkStructDictInheritance"); public static final DiagnosticGroup CHECK_VARIABLES = DiagnosticGroups.registerGroup( "checkVars", VarCheck.UNDEFINED_VAR_ERROR, VarCheck.VAR_MULTIPLY_DECLARED_ERROR, VariableReferenceCheck.EARLY_REFERENCE, VariableReferenceCheck.REDECLARED_VARIABLE); public static final DiagnosticGroup CHECK_USELESS_CODE = DiagnosticGroups.registerGroup( "uselessCode", CheckSideEffects.USELESS_CODE_ERROR, CheckUnreachableCode.UNREACHABLE_CODE); public static final DiagnosticGroup CONST = DiagnosticGroups.registerGroup( "const", CheckAccessControls.CONST_PROPERTY_DELETED, CheckAccessControls.CONST_PROPERTY_REASSIGNED_VALUE, ConstCheck.CONST_REASSIGNED_VALUE_ERROR, NewTypeInference.CONST_REASSIGNED, NewTypeInference.CONST_PROPERTY_REASSIGNED); public static final DiagnosticGroup CONSTANT_PROPERTY = DiagnosticGroups.registerGroup( "constantProperty", CheckAccessControls.CONST_PROPERTY_DELETED, CheckAccessControls.CONST_PROPERTY_REASSIGNED_VALUE, NewTypeInference.CONST_PROPERTY_REASSIGNED); public static final DiagnosticGroup TYPE_INVALIDATION = DiagnosticGroups.registerGroup( "typeInvalidation", DisambiguateProperties.Warnings.INVALIDATION, DisambiguateProperties.Warnings.INVALIDATION_ON_TYPE); public static final DiagnosticGroup DUPLICATE_VARS = DiagnosticGroups.registerGroup( "duplicate", VarCheck.VAR_MULTIPLY_DECLARED_ERROR, TypeValidator.DUP_VAR_DECLARATION, TypeValidator.DUP_VAR_DECLARATION_TYPE_MISMATCH, VariableReferenceCheck.REDECLARED_VARIABLE, GlobalTypeInfo.REDECLARED_PROPERTY); public static final DiagnosticGroup ES3 = DiagnosticGroups.registerGroup( "es3", RhinoErrorReporter.INVALID_ES3_PROP_NAME, RhinoErrorReporter.TRAILING_COMMA); static final DiagnosticGroup ES5_STRICT_UNCOMMON = DiagnosticGroups.registerGroup( "es5StrictUncommon", RhinoErrorReporter.INVALID_OCTAL_LITERAL, StrictModeCheck.USE_OF_WITH, StrictModeCheck.UNKNOWN_VARIABLE, StrictModeCheck.EVAL_DECLARATION, StrictModeCheck.EVAL_ASSIGNMENT, StrictModeCheck.ARGUMENTS_DECLARATION, StrictModeCheck.ARGUMENTS_ASSIGNMENT, StrictModeCheck.DELETE_VARIABLE, StrictModeCheck.DUPLICATE_OBJECT_KEY, StrictModeCheck.BAD_FUNCTION_DECLARATION); static final DiagnosticGroup ES5_STRICT_REFLECTION = DiagnosticGroups.registerGroup( "es5StrictReflection", StrictModeCheck.ARGUMENTS_CALLEE_FORBIDDEN, StrictModeCheck.ARGUMENTS_CALLER_FORBIDDEN, StrictModeCheck.FUNCTION_CALLER_FORBIDDEN, StrictModeCheck.FUNCTION_ARGUMENTS_PROP_FORBIDDEN); public static final DiagnosticGroup ES5_STRICT = DiagnosticGroups.registerGroup("es5Strict", ES5_STRICT_UNCOMMON, ES5_STRICT_REFLECTION); public static final DiagnosticGroup MISSING_PROVIDE = DiagnosticGroups.registerGroup("missingProvide", CheckProvides.MISSING_PROVIDE_WARNING); public static final DiagnosticGroup MISSING_REQUIRE = DiagnosticGroups.registerGroup( "missingRequire", CheckRequiresForConstructors.MISSING_REQUIRE_WARNING); public static final DiagnosticGroup EXTRA_REQUIRE = DiagnosticGroups.registerGroup( "extraRequire", CheckRequiresForConstructors.EXTRA_REQUIRE_WARNING); public static final DiagnosticGroup MISSING_GETCSSNAME = DiagnosticGroups.registerGroup( "missingGetCssName", CheckMissingGetCssName.MISSING_GETCSSNAME); public static final DiagnosticGroup DUPLICATE_MESSAGE = DiagnosticGroups.registerGroup("duplicateMessage", JsMessageVisitor.MESSAGE_DUPLICATE_KEY); public static final DiagnosticGroup MESSAGE_DESCRIPTIONS = DiagnosticGroups.registerGroup( "msgDescriptions", JsMessageVisitor.MESSAGE_HAS_NO_DESCRIPTION); public static final DiagnosticGroup MISPLACED_TYPE_ANNOTATION = DiagnosticGroups.registerGroup( "misplacedTypeAnnotation", CheckJSDoc.DISALLOWED_MEMBER_JSDOC, CheckJSDoc.MISPLACED_ANNOTATION, CheckJSDoc.MISPLACED_MSG_ANNOTATION); public static final DiagnosticGroup SUSPICIOUS_CODE = DiagnosticGroups.registerGroup( "suspiciousCode", CheckSuspiciousCode.SUSPICIOUS_SEMICOLON, CheckSuspiciousCode.SUSPICIOUS_COMPARISON_WITH_NAN, CheckSuspiciousCode.SUSPICIOUS_IN_OPERATOR, CheckSuspiciousCode.SUSPICIOUS_INSTANCEOF_LEFT_OPERAND); public static final DiagnosticGroup DEPRECATED_ANNOTATIONS = DiagnosticGroups.registerGroup("deprecatedAnnotations", CheckJSDoc.ANNOTATION_DEPRECATED); // These checks are not intended to be enabled as errors. It is // recommended that you think of them as "linter" warnings that // provide optional suggestions. public static final DiagnosticGroup LINT_CHECKS = DiagnosticGroups.registerGroup( "lintChecks", // undocumented CheckEmptyStatements.USELESS_EMPTY_STATEMENT, CheckEnums.DUPLICATE_ENUM_VALUE, // TODO(tbreisacher): Consider moving the CheckInterfaces warnings into the // checkTypes DiagnosticGroup CheckInterfaces.INTERFACE_FUNCTION_NOT_EMPTY, CheckInterfaces.INTERFACE_SHOULD_NOT_TAKE_ARGS, CheckJSDocStyle.MISSING_PARAM_JSDOC, CheckJSDocStyle.MUST_BE_PRIVATE, CheckJSDocStyle.OPTIONAL_PARAM_NOT_MARKED_OPTIONAL, CheckJSDocStyle.OPTIONAL_TYPE_NOT_USING_OPTIONAL_NAME, CheckNullableReturn.NULLABLE_RETURN, CheckNullableReturn.NULLABLE_RETURN_WITH_NAME, CheckForInOverArray.FOR_IN_OVER_ARRAY, CheckPrototypeProperties.ILLEGAL_PROTOTYPE_MEMBER, ImplicitNullabilityCheck.IMPLICITLY_NULLABLE_JSDOC, RhinoErrorReporter.JSDOC_MISSING_BRACES_WARNING, RhinoErrorReporter.JSDOC_MISSING_TYPE_WARNING, RhinoErrorReporter.TOO_MANY_TEMPLATE_PARAMS); public static final DiagnosticGroup USE_OF_GOOG_BASE = DiagnosticGroups.registerGroup("useOfGoogBase", ProcessClosurePrimitives.USE_OF_GOOG_BASE); public static final DiagnosticGroup CLOSURE_DEP_METHOD_USAGE_CHECKS = DiagnosticGroups.registerGroup( "closureDepMethodUsageChecks", ProcessClosurePrimitives.INVALID_CLOSURE_CALL_ERROR); // This group exists so that generated code can suppress these // warnings. Not for general use. These diagnostics will most likely // be moved to the suspiciousCode group. static { DiagnosticGroups.registerGroup( "transitionalSuspiciousCodeWarnings", PeepholeFoldConstants.INDEX_OUT_OF_BOUNDS_ERROR, PeepholeFoldConstants.NEGATING_A_NON_NUMBER_ERROR, PeepholeFoldConstants.BITWISE_OPERAND_OUT_OF_RANGE, PeepholeFoldConstants.SHIFT_AMOUNT_OUT_OF_BOUNDS, PeepholeFoldConstants.FRACTIONAL_BITWISE_OPERAND); } public static final DiagnosticGroup CONFORMANCE_VIOLATIONS = DiagnosticGroups.registerGroup( "conformanceViolations", CheckConformance.CONFORMANCE_VIOLATION, CheckConformance.CONFORMANCE_POSSIBLE_VIOLATION); static { // For internal use only, so there is no constant for it. DiagnosticGroups.registerGroup( "invalidProvide", ProcessClosurePrimitives.INVALID_PROVIDE_ERROR); DiagnosticGroups.registerGroup("es6Typed", RhinoErrorReporter.MISPLACED_TYPE_SYNTAX); } /** Adds warning levels by name. */ void setWarningLevel(CompilerOptions options, String name, CheckLevel level) { DiagnosticGroup group = forName(name); Preconditions.checkNotNull(group, "No warning class for name: %s", name); options.setWarningLevel(group, level); } }
/** * 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); } }
/** Insures '@constructor X' has a 'goog.provide("X")' . */ class CheckProvides implements HotSwapCompilerPass { private final AbstractCompiler compiler; private final CheckLevel checkLevel; private final CodingConvention codingConvention; static final DiagnosticType MISSING_PROVIDE_WARNING = DiagnosticType.disabled("JSC_MISSING_PROVIDE", "missing goog.provide(''{0}'')"); CheckProvides(AbstractCompiler compiler, CheckLevel checkLevel) { this.compiler = compiler; this.checkLevel = checkLevel; this.codingConvention = compiler.getCodingConvention(); } @Override public void process(Node externs, Node root) { hotSwapScript(root); } @Override public void hotSwapScript(Node scriptRoot) { CheckProvidesCallback callback = new CheckProvidesCallback(codingConvention); new NodeTraversal(compiler, callback).traverse(scriptRoot); } private class CheckProvidesCallback extends AbstractShallowCallback { private final Map<String, Node> provides = Maps.newHashMap(); private final Map<String, Node> ctors = Maps.newHashMap(); private final CodingConvention convention; CheckProvidesCallback(CodingConvention convention) { this.convention = convention; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.CALL: String providedClassName = codingConvention.extractClassNameIfProvide(n, parent); if (providedClassName != null) { provides.put(providedClassName, n); } break; case Token.FUNCTION: visitFunctionNode(n, parent); break; case Token.SCRIPT: visitScriptNode(t, n); } } private void visitFunctionNode(Node n, Node parent) { Node name = null; JSDocInfo info = parent.getJSDocInfo(); if (info != null && info.isConstructor()) { name = parent.getFirstChild(); } else { // look to the child, maybe it's a named function info = n.getJSDocInfo(); if (info != null && info.isConstructor()) { name = n.getFirstChild(); } } if (name != null && name.isQualifiedName()) { String qualifiedName = name.getQualifiedName(); if (!this.convention.isPrivate(qualifiedName)) { Visibility visibility = info.getVisibility(); if (!visibility.equals(JSDocInfo.Visibility.PRIVATE)) { ctors.put(qualifiedName, name); } } } } private void visitScriptNode(NodeTraversal t, Node n) { for (String ctorName : ctors.keySet()) { if (!provides.containsKey(ctorName)) { compiler.report( t.makeError(ctors.get(ctorName), checkLevel, MISSING_PROVIDE_WARNING, ctorName)); } } provides.clear(); ctors.clear(); } } }
/** A compiler pass to run the type inference analysis. */ class TypeInferencePass implements CompilerPass { static final DiagnosticType DATAFLOW_ERROR = DiagnosticType.warning("JSC_INTERNAL_ERROR_DATAFLOW", "non-monotonic data-flow analysis"); private final AbstractCompiler compiler; private final ReverseAbstractInterpreter reverseInterpreter; private Scope topScope; private ScopeCreator scopeCreator; private final Map<String, AssertionFunctionSpec> assertionFunctionsMap; /** * Local variables that are declared in an outer scope, but are assigned in an inner scope. We * cannot do type inference on these vars. */ private final Multimap<Scope, Var> escapedLocalVars = HashMultimap.create(); TypeInferencePass( AbstractCompiler compiler, ReverseAbstractInterpreter reverseInterpreter, Scope topScope, ScopeCreator scopeCreator) { this.compiler = compiler; this.reverseInterpreter = reverseInterpreter; this.topScope = topScope; this.scopeCreator = scopeCreator; assertionFunctionsMap = Maps.newHashMap(); for (AssertionFunctionSpec assertionFucntion : compiler.getCodingConvention().getAssertionFunctions()) { assertionFunctionsMap.put(assertionFucntion.getFunctionName(), assertionFucntion); } } /** * Main entry point for type inference when running over the whole tree. * * @param externsRoot The root of the externs parse tree. * @param jsRoot The root of the input parse tree to be checked. */ public void process(Node externsRoot, Node jsRoot) { Node externsAndJs = jsRoot.getParent(); Preconditions.checkState(externsAndJs != null); Preconditions.checkState(externsRoot == null || externsAndJs.hasChild(externsRoot)); inferTypes(externsAndJs); } /** Entry point for type inference when running over part of the tree. */ void inferTypes(Node node) { NodeTraversal inferTypes = new NodeTraversal(compiler, new TypeInferringCallback(), scopeCreator); inferTypes.traverseWithScope(node, topScope); } private Collection<Var> getUnflowableVars(Scope scope) { List<Var> vars = Lists.newArrayList(); for (Scope current = scope; current.isLocal(); current = current.getParent()) { vars.addAll(escapedLocalVars.get(current)); } return vars; } void inferTypes(NodeTraversal t, Node n, Scope scope) { TypeInference typeInference = new TypeInference( compiler, computeCfg(n), reverseInterpreter, scope, assertionFunctionsMap, getUnflowableVars(scope)); try { typeInference.analyze(); escapedLocalVars.putAll(typeInference.getAssignedOuterLocalVars()); // Resolve any new type names found during the inference. compiler.getTypeRegistry().resolveTypesInScope(scope); } catch (DataFlowAnalysis.MaxIterationsExceededException e) { compiler.report(t.makeError(n, DATAFLOW_ERROR)); } } private class TypeInferringCallback implements ScopedCallback { public void enterScope(NodeTraversal t) { Scope scope = t.getScope(); Node node = t.getCurrentNode(); if (scope.isGlobal()) { inferTypes(t, node, scope); } } public void exitScope(NodeTraversal t) { Scope scope = t.getScope(); Node node = t.getCurrentNode(); if (scope.isLocal()) { inferTypes(t, node, scope); } } public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } public void visit(NodeTraversal t, Node n, Node parent) { // Do nothing } } private ControlFlowGraph<Node> computeCfg(Node n) { ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false); cfa.process(null, n); return cfa.getCfg(); } }
/** * 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(); } } }
/** * Flattens global objects/namespaces by replacing each '.' with '$' in their names. This reduces * the number of property lookups the browser has to do and allows the {@link RenameVars} pass to * shorten namespaced names. For example, goog.events.handleEvent() -> goog$events$handleEvent() -> * Za(). * * <p>If a global object's name is assigned to more than once, or if a property is added to the * global object in a complex expression, then none of its properties will be collapsed (for * safety/correctness). * * <p>If, after a global object is declared, it is never referenced except when its properties are * read or set, then the object will be removed after its properties have been collapsed. * * <p>Uninitialized variable stubs are created at a global object's declaration site for any of its * properties that are added late in a local scope. * * <p>Static properties of constructors are always collapsed, unsafely! For other objects: if, after * an object is declared, it is referenced directly in a way that might create an alias for it, then * none of its properties will be collapsed. This behavior is a safeguard to prevent the values * associated with the flattened names from getting out of sync with the object's actual property * values. For example, in the following case, an alias a$b, if created, could easily keep the value * 0 even after a.b became 5: <code> a = {b: 0}; c = a; c.b = 5; </code>. * * <p>This pass doesn't flatten property accesses of the form: a[b]. * * <p>For lots of examples, see the unit test. */ class CollapseProperties implements CompilerPass { // Warnings static final DiagnosticType UNSAFE_NAMESPACE_WARNING = DiagnosticType.warning("JSC_UNSAFE_NAMESPACE", "incomplete alias created for namespace {0}"); static final DiagnosticType NAMESPACE_REDEFINED_WARNING = DiagnosticType.warning("JSC_NAMESPACE_REDEFINED", "namespace {0} should not be redefined"); static final DiagnosticType UNSAFE_THIS = DiagnosticType.warning("JSC_UNSAFE_THIS", "dangerous use of 'this' in static method {0}"); static final DiagnosticType UNSAFE_CTOR_ALIASING = DiagnosticType.warning( "JSC_UNSAFE_CTOR_ALIASING", "Variable {0} aliases a constructor, " + "so it cannot be assigned multiple times"); private AbstractCompiler compiler; /** Global namespace tree */ private List<Name> globalNames; /** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */ private Map<String, Name> nameMap; private final boolean inlineAliases; /** * @param inlineAliases Whether we're allowed to inline local aliases of namespaces, etc. It's set * to false only by the deprecated property- renaming policies {@code HEURISTIC} and {@code * AGGRESSIVE_HEURISTIC}. */ CollapseProperties(AbstractCompiler compiler, boolean inlineAliases) { this.compiler = compiler; this.inlineAliases = inlineAliases; } @Override public void process(Node externs, Node root) { GlobalNamespace namespace; namespace = new GlobalNamespace(compiler, root); if (inlineAliases) { inlineAliases(namespace); } nameMap = namespace.getNameIndex(); globalNames = namespace.getNameForest(); checkNamespaces(); for (Name name : globalNames) { flattenReferencesToCollapsibleDescendantNames(name, name.getBaseName()); } // We collapse property definitions after collapsing property references // because this step can alter the parse tree above property references, // invalidating the node ancestry stored with each reference. for (Name name : globalNames) { collapseDeclarationOfNameAndDescendants(name, name.getBaseName()); } } /** * For each qualified name N in the global scope, we check if: (a) No ancestor of N is ever * aliased or assigned an unknown value type. (If N = "a.b.c", "a" and "a.b" are never aliased). * (b) N has exactly one write, and it lives in the global scope. (c) N is aliased in a local * scope. (d) N is aliased in global scope * * <p>If (a) is true, then GlobalNamespace must know all the writes to N. If (a) and (b) are true, * then N cannot change during the execution of a local scope. If (a) and (b) and (c) are true, * then the alias can be inlined if the alias obeys the usual rules for how we decide whether a * variable is inlineable. If (a) and (b) and (d) are true, then inline the alias if possible (if * it is assigned exactly once unconditionally). * * @see InlineVariables */ private void inlineAliases(GlobalNamespace namespace) { // Invariant: All the names in the worklist meet condition (a). Deque<Name> workList = new ArrayDeque<>(namespace.getNameForest()); while (!workList.isEmpty()) { Name name = workList.pop(); // Don't attempt to inline a getter or setter property as a variable. if (name.type == Name.Type.GET || name.type == Name.Type.SET) { continue; } if (!name.inExterns && name.globalSets == 1 && name.localSets == 0 && name.aliasingGets > 0) { // {@code name} meets condition (b). Find all of its local aliases // and try to inline them. List<Ref> refs = new ArrayList<>(name.getRefs()); for (Ref ref : refs) { if (ref.type == Type.ALIASING_GET && ref.scope.isLocal()) { // {@code name} meets condition (c). Try to inline it. // TODO(johnlenz): consider picking up new aliases at the end // of the pass instead of immediately like we do for global // inlines. if (inlineAliasIfPossible(name, ref, namespace)) { name.removeRef(ref); } } else if (ref.type == Type.ALIASING_GET && ref.scope.isGlobal() && ref.getTwin() == null) { // ignore aliases in chained assignments if (inlineGlobalAliasIfPossible(name, ref, namespace)) { name.removeRef(ref); } } } } // Check if {@code name} has any aliases left after the // local-alias-inlining above. if ((name.type == Name.Type.OBJECTLIT || name.type == Name.Type.FUNCTION) && name.aliasingGets == 0 && name.props != null) { // All of {@code name}'s children meet condition (a), so they can be // added to the worklist. workList.addAll(name.props); } } } /** * Attempt to inline an global alias of a global name. This requires that the name is well * defined: assigned unconditionally, assigned exactly once. It is assumed that, the name for * which it is an alias must already meet these same requirements. * * @param alias The alias to inline * @return Whether the alias was inlined. */ private boolean inlineGlobalAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) { // Ensure that the alias is assigned to global name at that the // declaration. Node aliasParent = alias.node.getParent(); if (aliasParent.isAssign() && NodeUtil.isExecutedExactlyOnce(aliasParent) // We special-case for constructors here, to inline constructor aliases // more aggressively in global scope. // We do this because constructor properties are always collapsed, // so we want to inline the aliases also to avoid breakages. || aliasParent.isName() && name.isConstructor()) { Node lvalue = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild(); if (!lvalue.isQualifiedName()) { return false; } name = namespace.getSlot(lvalue.getQualifiedName()); if (name != null && name.isInlinableGlobalAlias()) { Set<AstChange> newNodes = new LinkedHashSet<>(); List<Ref> refs = new ArrayList<>(name.getRefs()); for (Ref ref : refs) { switch (ref.type) { case SET_FROM_GLOBAL: continue; case DIRECT_GET: case ALIASING_GET: Node newNode = alias.node.cloneTree(); Node node = ref.node; node.getParent().replaceChild(node, newNode); newNodes.add(new AstChange(ref.module, ref.scope, newNode)); name.removeRef(ref); break; default: throw new IllegalStateException(); } } rewriteAliasProps(name, alias.node, 0, newNodes); // just set the original alias to null. aliasParent.replaceChild(alias.node, IR.nullNode()); compiler.reportCodeChange(); // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); return true; } } return false; } /** * @param name The Name whose properties references should be updated. * @param value The value to use when rewriting. * @param depth The chain depth. * @param newNodes Expression nodes that have been updated. */ private static void rewriteAliasProps(Name name, Node value, int depth, Set<AstChange> newNodes) { if (name.props == null) { return; } Preconditions.checkState(!value.matchesQualifiedName(name.getFullName())); for (Name prop : name.props) { rewriteAliasProps(prop, value, depth + 1, newNodes); List<Ref> refs = new ArrayList<>(prop.getRefs()); for (Ref ref : refs) { Node target = ref.node; for (int i = 0; i <= depth; i++) { if (target.isGetProp()) { target = target.getFirstChild(); } else if (NodeUtil.isObjectLitKey(target)) { // Object literal key definitions are a little trickier, as we // need to find the assignment target Node gparent = target.getParent().getParent(); if (gparent.isAssign()) { target = gparent.getFirstChild(); } else { Preconditions.checkState(NodeUtil.isObjectLitKey(gparent)); target = gparent; } } else { throw new IllegalStateException("unexpected: " + target); } } Preconditions.checkState(target.isGetProp() || target.isName()); target.getParent().replaceChild(target, value.cloneTree()); prop.removeRef(ref); // Rescan the expression root. newNodes.add(new AstChange(ref.module, ref.scope, ref.node)); } } } private boolean inlineAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) { // Ensure that the alias is assigned to a local variable at that // variable's declaration. If the alias's parent is a NAME, // then the NAME must be the child of a VAR node, and we must // be in a VAR assignment. Node aliasParent = alias.node.getParent(); if (aliasParent.isName()) { // Ensure that the local variable is well defined and never reassigned. Scope scope = alias.scope; String aliasVarName = aliasParent.getString(); Var aliasVar = scope.getVar(aliasVarName); ReferenceCollectingCallback collector = new ReferenceCollectingCallback( compiler, ReferenceCollectingCallback.DO_NOTHING_BEHAVIOR, Predicates.equalTo(aliasVar)); collector.processScope(scope); ReferenceCollection aliasRefs = collector.getReferences(aliasVar); Set<AstChange> newNodes = new LinkedHashSet<>(); if (aliasRefs.isWellDefined() && aliasRefs.firstReferenceIsAssigningDeclaration()) { if (!aliasRefs.isAssignedOnceInLifetime()) { // Static properties of constructors are always collapsed. // So, if a constructor is aliased and its properties are accessed from // the alias, we would like to inline the alias here to access the // properties correctly. // But if the aliased variable is assigned more than once, we can't // inline, so we warn. if (name.isConstructor()) { boolean accessPropsAfterAliasing = false; for (Reference ref : aliasRefs.references) { if (ref.getNode().getParent().isGetProp()) { accessPropsAfterAliasing = true; break; } } if (accessPropsAfterAliasing) { compiler.report(JSError.make(aliasParent, UNSAFE_CTOR_ALIASING, aliasVarName)); } } return false; } // The alias is well-formed, so do the inlining now. int size = aliasRefs.references.size(); for (int i = 1; i < size; i++) { ReferenceCollectingCallback.Reference aliasRef = aliasRefs.references.get(i); Node newNode = alias.node.cloneTree(); aliasRef.getParent().replaceChild(aliasRef.getNode(), newNode); newNodes.add(new AstChange(getRefModule(aliasRef), aliasRef.getScope(), newNode)); } // just set the original alias to null. aliasParent.replaceChild(alias.node, IR.nullNode()); compiler.reportCodeChange(); // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); return true; } } return false; } JSModule getRefModule(ReferenceCollectingCallback.Reference ref) { CompilerInput input = compiler.getInput(ref.getInputId()); return input == null ? null : input.getModule(); } /** * Runs through all namespaces (prefixes of classes and enums), and checks if any of them have * been used in an unsafe way. */ private void checkNamespaces() { for (Name name : nameMap.values()) { if (name.isNamespaceObjectLit() && (name.aliasingGets > 0 || name.localSets + name.globalSets > 1 || name.deleteProps > 0)) { boolean initialized = name.getDeclaration() != null; for (Ref ref : name.getRefs()) { if (ref == name.getDeclaration()) { continue; } if (ref.type == Ref.Type.DELETE_PROP) { if (initialized) { warnAboutNamespaceRedefinition(name, ref); } } else if (ref.type == Ref.Type.SET_FROM_GLOBAL || ref.type == Ref.Type.SET_FROM_LOCAL) { if (initialized && !isSafeNamespaceReinit(ref)) { warnAboutNamespaceRedefinition(name, ref); } initialized = true; } else if (ref.type == Ref.Type.ALIASING_GET) { warnAboutNamespaceAliasing(name, ref); } } } } } private boolean isSafeNamespaceReinit(Ref ref) { // allow "a = a || {}" or "var a = a || {}" Node valParent = getValueParent(ref); Node val = valParent.getLastChild(); if (val.getType() == Token.OR) { Node maybeName = val.getFirstChild(); if (ref.node.matchesQualifiedName(maybeName)) { return true; } } return false; } /** * Gets the parent node of the value for any assignment to a Name. For example, in the assignment * {@code var x = 3;} the parent would be the NAME node. */ private static Node getValueParent(Ref ref) { // there are two types of declarations: VARs and ASSIGNs return (ref.node.getParent() != null && ref.node.getParent().isVar()) ? ref.node : ref.node.getParent(); } /** * Reports a warning because a namespace was aliased. * * @param nameObj A namespace that is being aliased * @param ref The reference that forced the alias */ private void warnAboutNamespaceAliasing(Name nameObj, Ref ref) { compiler.report(JSError.make(ref.node, UNSAFE_NAMESPACE_WARNING, nameObj.getFullName())); } /** * Reports a warning because a namespace was redefined. * * @param nameObj A namespace that is being redefined * @param ref The reference that set the namespace */ private void warnAboutNamespaceRedefinition(Name nameObj, Ref ref) { compiler.report(JSError.make(ref.node, NAMESPACE_REDEFINED_WARNING, nameObj.getFullName())); } /** * Flattens all references to collapsible properties of a global name except their initial * definitions. Recurs on subnames. * * @param n An object representing a global name * @param alias The flattened name for {@code n} */ private void flattenReferencesToCollapsibleDescendantNames(Name n, String alias) { if (n.props == null || n.isCollapsingExplicitlyDenied()) { return; } for (Name p : n.props) { String propAlias = appendPropForAlias(alias, p.getBaseName()); if (p.canCollapse()) { flattenReferencesTo(p, propAlias); } else if (p.isSimpleStubDeclaration() && !p.isCollapsingExplicitlyDenied()) { flattenSimpleStubDeclaration(p, propAlias); } flattenReferencesToCollapsibleDescendantNames(p, propAlias); } } /** Flattens a stub declaration. This is mostly a hack to support legacy users. */ private void flattenSimpleStubDeclaration(Name name, String alias) { Ref ref = Iterables.getOnlyElement(name.getRefs()); Node nameNode = NodeUtil.newName(compiler, alias, ref.node, name.getFullName()); Node varNode = IR.var(nameNode).useSourceInfoIfMissingFrom(nameNode); Preconditions.checkState(ref.node.getParent().isExprResult()); Node parent = ref.node.getParent(); Node grandparent = parent.getParent(); grandparent.replaceChild(parent, varNode); compiler.reportCodeChange(); } /** * Flattens all references to a collapsible property of a global name except its initial * definition. * * @param n A global property name (e.g. "a.b" or "a.b.c.d") * @param alias The flattened name (e.g. "a$b" or "a$b$c$d") */ private void flattenReferencesTo(Name n, String alias) { String originalName = n.getFullName(); for (Ref r : n.getRefs()) { if (r == n.getDeclaration()) { // Declarations are handled separately. continue; } Node rParent = r.node.getParent(); // There are two cases when we shouldn't flatten a reference: // 1) Object literal keys, because duplicate keys show up as refs. // 2) References inside a complex assign. (a = x.y = 0). These are // called TWIN references, because they show up twice in the // reference list. Only collapse the set, not the alias. if (!NodeUtil.isObjectLitKey(r.node) && (r.getTwin() == null || r.isSet())) { flattenNameRef(alias, r.node, rParent, originalName); } } // Flatten all occurrences of a name as a prefix of its subnames. For // example, if {@code n} corresponds to the name "a.b", then "a.b" will be // replaced with "a$b" in all occurrences of "a.b.c", "a.b.c.d", etc. if (n.props != null) { for (Name p : n.props) { flattenPrefixes(alias, p, 1); } } } /** * Flattens all occurrences of a name as a prefix of subnames beginning with a particular subname. * * @param n A global property name (e.g. "a.b.c.d") * @param alias A flattened prefix name (e.g. "a$b") * @param depth The difference in depth between the property name and the prefix name (e.g. 2) */ private void flattenPrefixes(String alias, Name n, int depth) { // Only flatten the prefix of a name declaration if the name being // initialized is fully qualified (i.e. not an object literal key). String originalName = n.getFullName(); Ref decl = n.getDeclaration(); if (decl != null && decl.node != null && decl.node.isGetProp()) { flattenNameRefAtDepth(alias, decl.node, depth, originalName); } for (Ref r : n.getRefs()) { if (r == decl) { // Declarations are handled separately. continue; } // References inside a complex assign (a = x.y = 0) // have twins. We should only flatten one of the twins. if (r.getTwin() == null || r.isSet()) { flattenNameRefAtDepth(alias, r.node, depth, originalName); } } if (n.props != null) { for (Name p : n.props) { flattenPrefixes(alias, p, depth + 1); } } } /** * Flattens a particular prefix of a single name reference. * * @param alias A flattened prefix name (e.g. "a$b") * @param n The node corresponding to a subproperty name (e.g. "a.b.c.d") * @param depth The difference in depth between the property name and the prefix name (e.g. 2) * @param originalName String version of the property name. */ private void flattenNameRefAtDepth(String alias, Node n, int depth, String originalName) { // This method has to work for both GETPROP chains and, in rare cases, // OBJLIT keys, possibly nested. That's why we check for children before // proceeding. In the OBJLIT case, we don't need to do anything. int nType = n.getType(); boolean isQName = nType == Token.NAME || nType == Token.GETPROP; boolean isObjKey = NodeUtil.isObjectLitKey(n); Preconditions.checkState(isObjKey || isQName); if (isQName) { for (int i = 1; i < depth && n.hasChildren(); i++) { n = n.getFirstChild(); } if (n.isGetProp() && n.getFirstChild().isGetProp()) { flattenNameRef(alias, n.getFirstChild(), n, originalName); } } } /** * Replaces a GETPROP a.b.c with a NAME a$b$c. * * @param alias A flattened prefix name (e.g. "a$b") * @param n The GETPROP node corresponding to the original name (e.g. "a.b") * @param parent {@code n}'s parent * @param originalName String version of the property name. */ private void flattenNameRef(String alias, Node n, Node parent, String originalName) { Preconditions.checkArgument( n.isGetProp(), "Expected GETPROP, found %s. Node: %s", Token.name(n.getType()), n); // BEFORE: // getprop // getprop // name a // string b // string c // AFTER: // name a$b$c Node ref = NodeUtil.newName(compiler, alias, n, originalName); NodeUtil.copyNameAnnotations(n.getLastChild(), ref); if (parent.isCall() && n == parent.getFirstChild()) { // The node was a call target, we are deliberately flatten these as // we node the "this" isn't provided by the namespace. Mark it as such: parent.putBooleanProp(Node.FREE_CALL, true); } TypeI type = n.getTypeI(); if (type != null) { ref.setTypeI(type); } parent.replaceChild(n, ref); compiler.reportCodeChange(); } /** * Collapses definitions of the collapsible properties of a global name. Recurs on subnames that * also represent JavaScript objects with collapsible properties. * * @param n A node representing a global name * @param alias The flattened name for {@code n} */ private void collapseDeclarationOfNameAndDescendants(Name n, String alias) { boolean canCollapseChildNames = n.canCollapseUnannotatedChildNames(); // Handle this name first so that nested object literals get unrolled. if (n.canCollapse()) { updateObjLitOrFunctionDeclaration(n, alias, canCollapseChildNames); } if (n.props == null) { return; } for (Name p : n.props) { // Recur first so that saved node ancestries are intact when needed. collapseDeclarationOfNameAndDescendants(p, appendPropForAlias(alias, p.getBaseName())); if (!p.inExterns && canCollapseChildNames && p.getDeclaration() != null && p.canCollapse() && p.getDeclaration().node != null && p.getDeclaration().node.getParent() != null && p.getDeclaration().node.getParent().isAssign()) { updateSimpleDeclaration(appendPropForAlias(alias, p.getBaseName()), p, p.getDeclaration()); } } } /** * Updates the initial assignment to a collapsible property at global scope by changing it to a * variable declaration (e.g. a.b = 1 -> var a$b = 1). The property's value may either be a * primitive or an object literal or function whose properties aren't collapsible. * * @param alias The flattened property name (e.g. "a$b") * @param refName The name for the reference being updated. * @param ref An object containing information about the assignment getting updated */ private void updateSimpleDeclaration(String alias, Name refName, Ref ref) { Node rvalue = ref.node.getNext(); Node parent = ref.node.getParent(); Node grandparent = parent.getParent(); Node greatGrandparent = grandparent.getParent(); if (rvalue != null && rvalue.isFunction()) { checkForHosedThisReferences(rvalue, refName.docInfo, refName); } // Create the new alias node. Node nameNode = NodeUtil.newName(compiler, alias, grandparent.getFirstChild(), refName.getFullName()); NodeUtil.copyNameAnnotations(ref.node.getLastChild(), nameNode); if (grandparent.isExprResult()) { // BEFORE: a.b.c = ...; // exprstmt // assign // getprop // getprop // name a // string b // string c // NODE // AFTER: var a$b$c = ...; // var // name a$b$c // NODE // Remove the r-value (NODE). parent.removeChild(rvalue); nameNode.addChildToFront(rvalue); Node varNode = IR.var(nameNode); greatGrandparent.replaceChild(grandparent, varNode); } else { // This must be a complex assignment. Preconditions.checkNotNull(ref.getTwin()); // BEFORE: // ... (x.y = 3); // // AFTER: // var x$y; // ... (x$y = 3); Node current = grandparent; Node currentParent = grandparent.getParent(); for (; !currentParent.isScript() && !currentParent.isBlock(); current = currentParent, currentParent = currentParent.getParent()) {} // Create a stub variable declaration right // before the current statement. Node stubVar = IR.var(nameNode.cloneTree()).useSourceInfoIfMissingFrom(nameNode); currentParent.addChildBefore(stubVar, current); parent.replaceChild(ref.node, nameNode); } compiler.reportCodeChange(); } /** * Updates the first initialization (a.k.a "declaration") of a global name. This involves * flattening the global name (if it's not just a global variable name already), collapsing object * literal keys into global variables, declaring stub global variables for properties added later * in a local scope. * * <p>It may seem odd that this function also takes care of declaring stubs for direct children. * The ultimate goal of this function is to eliminate the global name entirely (when possible), so * that "middlemen" namespaces disappear, and to do that we need to make sure that all the direct * children will be collapsed as well. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c") * @param canCollapseChildNames Whether it's possible to collapse children of this name. (This is * mostly passed for convenience; it's equivalent to n.canCollapseChildNames()). */ private void updateObjLitOrFunctionDeclaration( Name n, String alias, boolean canCollapseChildNames) { Ref decl = n.getDeclaration(); if (decl == null) { // Some names do not have declarations, because they // are only defined in local scopes. return; } if (decl.getTwin() != null) { // Twin declarations will get handled when normal references // are handled. return; } switch (decl.node.getParent().getType()) { case Token.ASSIGN: updateObjLitOrFunctionDeclarationAtAssignNode(n, alias, canCollapseChildNames); break; case Token.VAR: updateObjLitOrFunctionDeclarationAtVarNode(n, canCollapseChildNames); break; case Token.FUNCTION: updateFunctionDeclarationAtFunctionNode(n, canCollapseChildNames); break; } } /** * Updates the first initialization (a.k.a "declaration") of a global name that occurs at an * ASSIGN node. See comment for {@link #updateObjLitOrFunctionDeclaration}. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c") */ private void updateObjLitOrFunctionDeclarationAtAssignNode( Name n, String alias, boolean canCollapseChildNames) { // NOTE: It's important that we don't add additional nodes // (e.g. a var node before the exprstmt) because the exprstmt might be // the child of an if statement that's not inside a block). Ref ref = n.getDeclaration(); Node rvalue = ref.node.getNext(); Node varNode = new Node(Token.VAR); Node varParent = ref.node.getAncestor(3); Node grandparent = ref.node.getAncestor(2); boolean isObjLit = rvalue.isObjectLit(); boolean insertedVarNode = false; if (isObjLit && n.canEliminate()) { // Eliminate the object literal altogether. varParent.replaceChild(grandparent, varNode); ref.node = null; insertedVarNode = true; } else if (!n.isSimpleName()) { // Create a VAR node to declare the name. if (rvalue.isFunction()) { checkForHosedThisReferences(rvalue, n.docInfo, n); } ref.node.getParent().removeChild(rvalue); Node nameNode = NodeUtil.newName(compiler, alias, ref.node.getAncestor(2), n.getFullName()); JSDocInfo info = NodeUtil.getBestJSDocInfo(ref.node.getParent()); if (ref.node.getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME) || (info != null && info.isConstant())) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } if (info != null) { varNode.setJSDocInfo(info); } varNode.addChildToBack(nameNode); nameNode.addChildToFront(rvalue); varParent.replaceChild(grandparent, varNode); // Update the node ancestry stored in the reference. ref.node = nameNode; insertedVarNode = true; } if (canCollapseChildNames) { if (isObjLit) { declareVarsForObjLitValues( n, alias, rvalue, varNode, varParent.getChildBefore(varNode), varParent); } addStubsForUndeclaredProperties(n, alias, varParent, varNode); } if (insertedVarNode) { if (!varNode.hasChildren()) { varParent.removeChild(varNode); } compiler.reportCodeChange(); } } /** * Warns about any references to "this" in the given FUNCTION. The function is getting collapsed, * so the references will change. */ private void checkForHosedThisReferences(Node function, JSDocInfo docInfo, final Name name) { // A function is getting collapsed. Make sure that if it refers to // "this", it must be a constructor or documented with @this. if (docInfo == null || (!docInfo.isConstructor() && !docInfo.hasThisType())) { NodeTraversal.traverseEs6( compiler, function.getLastChild(), new NodeTraversal.AbstractShallowCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isThis()) { compiler.report(JSError.make(n, UNSAFE_THIS, name.getFullName())); } } }); } } /** * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a VAR * node. See comment for {@link #updateObjLitOrFunctionDeclaration}. * * @param n An object representing a global name (e.g. "a") */ private void updateObjLitOrFunctionDeclarationAtVarNode(Name n, boolean canCollapseChildNames) { if (!canCollapseChildNames) { return; } Ref ref = n.getDeclaration(); String name = ref.node.getString(); Node rvalue = ref.node.getFirstChild(); Node varNode = ref.node.getParent(); Node grandparent = varNode.getParent(); boolean isObjLit = rvalue.isObjectLit(); int numChanges = 0; if (isObjLit) { numChanges += declareVarsForObjLitValues( n, name, rvalue, varNode, grandparent.getChildBefore(varNode), grandparent); } numChanges += addStubsForUndeclaredProperties(n, name, grandparent, varNode); if (isObjLit && n.canEliminate()) { varNode.removeChild(ref.node); if (!varNode.hasChildren()) { grandparent.removeChild(varNode); } numChanges++; // Clear out the object reference, since we've eliminated it from the // parse tree. ref.node = null; } if (numChanges > 0) { compiler.reportCodeChange(); } } /** * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a * FUNCTION node. See comment for {@link #updateObjLitOrFunctionDeclaration}. * * @param n An object representing a global name (e.g. "a") */ private void updateFunctionDeclarationAtFunctionNode(Name n, boolean canCollapseChildNames) { if (!canCollapseChildNames || !n.canCollapse()) { return; } Ref ref = n.getDeclaration(); String fnName = ref.node.getString(); addStubsForUndeclaredProperties(n, fnName, ref.node.getAncestor(2), ref.node.getParent()); } /** * Declares global variables to serve as aliases for the values in an object literal, optionally * removing all of the object literal's keys and values. * * @param alias The object literal's flattened name (e.g. "a$b$c") * @param objlit The OBJLIT node * @param varNode The VAR node to which new global variables should be added as children * @param nameToAddAfter The child of {@code varNode} after which new variables should be added * (may be null) * @param varParent {@code varNode}'s parent * @return The number of variables added */ private int declareVarsForObjLitValues( Name objlitName, String alias, Node objlit, Node varNode, Node nameToAddAfter, Node varParent) { int numVars = 0; int arbitraryNameCounter = 0; boolean discardKeys = !objlitName.shouldKeepKeys(); for (Node key = objlit.getFirstChild(), nextKey; key != null; key = nextKey) { Node value = key.getFirstChild(); nextKey = key.getNext(); // A get or a set can not be rewritten as a VAR. if (key.isGetterDef() || key.isSetterDef()) { continue; } // We generate arbitrary names for keys that aren't valid JavaScript // identifiers, since those keys are never referenced. (If they were, // this object literal's child names wouldn't be collapsible.) The only // reason that we don't eliminate them entirely is the off chance that // their values are expressions that have side effects. boolean isJsIdentifier = !key.isNumber() && TokenStream.isJSIdentifier(key.getString()); String propName = isJsIdentifier ? key.getString() : String.valueOf(++arbitraryNameCounter); // If the name cannot be collapsed, skip it. String qName = objlitName.getFullName() + '.' + propName; Name p = nameMap.get(qName); if (p != null && !p.canCollapse()) { continue; } String propAlias = appendPropForAlias(alias, propName); Node refNode = null; if (discardKeys) { objlit.removeChild(key); value.detachFromParent(); } else { // Substitute a reference for the value. refNode = IR.name(propAlias); if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) { refNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } key.replaceChild(value, refNode); } // Declare the collapsed name as a variable with the original value. Node nameNode = IR.name(propAlias); nameNode.addChildToFront(value); if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(key); if (nameToAddAfter != null) { varParent.addChildAfter(newVar, nameToAddAfter); } else { varParent.addChildBefore(newVar, varNode); } compiler.reportCodeChange(); nameToAddAfter = newVar; // Update the global name's node ancestry if it hasn't already been // done. (Duplicate keys in an object literal can bring us here twice // for the same global name.) if (isJsIdentifier && p != null) { if (!discardKeys) { Ref newAlias = p.getDeclaration().cloneAndReclassify(Ref.Type.ALIASING_GET); newAlias.node = refNode; p.addRef(newAlias); } p.getDeclaration().node = nameNode; if (value.isFunction()) { checkForHosedThisReferences(value, key.getJSDocInfo(), p); } } numVars++; } return numVars; } /** * Adds global variable "stubs" for any properties of a global name that are only set in a local * scope or read but never set. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name of the object whose properties we are adding stubs for (e.g. * "a$b$c") * @param parent The node to which new global variables should be added as children * @param addAfter The child of after which new variables should be added * @return The number of variables added */ private int addStubsForUndeclaredProperties(Name n, String alias, Node parent, Node addAfter) { Preconditions.checkState(n.canCollapseUnannotatedChildNames()); Preconditions.checkArgument(NodeUtil.isStatementBlock(parent)); Preconditions.checkNotNull(addAfter); if (n.props == null) { return 0; } int numStubs = 0; for (Name p : n.props) { if (p.needsToBeStubbed()) { String propAlias = appendPropForAlias(alias, p.getBaseName()); Node nameNode = IR.name(propAlias); Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(addAfter); parent.addChildAfter(newVar, addAfter); addAfter = newVar; numStubs++; compiler.reportCodeChange(); // Determine if this is a constant var by checking the first // reference to it. Don't check the declaration, as it might be null. if (p.getRefs().get(0).node.getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME)) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } } } return numStubs; } private static String appendPropForAlias(String root, String prop) { if (prop.indexOf('$') != -1) { // Encode '$' in a property as '$0'. Because '0' cannot be the // start of an identifier, this will never conflict with our // encoding from '.' -> '$'. prop = prop.replace("$", "$0"); } return root + '$' + prop; } }
/** * 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; } }
/** * Ensures string literals matching certain patterns are only used as goog.getCssName parameters. * * @author [email protected] (Martin Kretzschmar) */ @GwtIncompatible("java.util.regex") class CheckMissingGetCssName extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; private final CheckLevel level; private final Matcher blacklist; static final String GET_CSS_NAME_FUNCTION = "goog.getCssName"; static final String GET_UNIQUE_ID_FUNCTION = ".getUniqueId"; static final DiagnosticType MISSING_GETCSSNAME = DiagnosticType.disabled( "JSC_MISSING_GETCSSNAME", "missing goog.getCssName around literal ''{0}''"); CheckMissingGetCssName(AbstractCompiler compiler, CheckLevel level, String blacklistRegex) { this.compiler = compiler; this.level = level; this.blacklist = Pattern.compile("\\b(?:" + blacklistRegex + ")").matcher(""); } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isString() && !parent.isGetProp() && !parent.isRegExp()) { String s = n.getString(); for (blacklist.reset(s); blacklist.find(); ) { if (parent.isTemplateLit()) { if (parent.getChildCount() > 1) { // Ignore template string with substitutions continue; } else { n = parent; } } if (insideGetCssNameCall(n)) { continue; } if (insideGetUniqueIdCall(n)) { continue; } if (insideAssignmentToIdConstant(n)) { continue; } compiler.report(t.makeError(n, level, MISSING_GETCSSNAME, blacklist.group())); } } } /** Returns whether the node is an argument of a goog.getCssName call. */ private static boolean insideGetCssNameCall(Node n) { Node parent = n.getParent(); return parent.isCall() && parent.getFirstChild().matchesQualifiedName(GET_CSS_NAME_FUNCTION); } /** * Returns whether the node is an argument of a function that returns a unique id (the last part * of the qualified name matches GET_UNIQUE_ID_FUNCTION). */ private static boolean insideGetUniqueIdCall(Node n) { Node parent = n.getParent(); String name = parent.isCall() ? parent.getFirstChild().getQualifiedName() : null; return name != null && name.endsWith(GET_UNIQUE_ID_FUNCTION); } /** * Returns whether the node is the right hand side of an assignment or initialization of a * variable named *_ID of *_ID_. */ private boolean insideAssignmentToIdConstant(Node n) { Node parent = n.getParent(); if (parent.isAssign()) { String qname = parent.getFirstChild().getQualifiedName(); return qname != null && isIdName(qname); } else if (parent.isName()) { Node grandParent = parent.getParent(); if (grandParent != null && NodeUtil.isNameDeclaration(grandParent)) { String name = parent.getString(); return isIdName(name); } else { return false; } } else { return false; } } private static boolean isIdName(String name) { return name.endsWith("ID") || name.endsWith("ID_"); } }
/** * 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()); } }
/** * 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(); } } }
/** * Checks for non side effecting statements such as * <pre> * var s = "this string is " * "continued on the next line but you forgot the +"; * x == foo(); // should that be '='? * foo();; // probably just a stray-semicolon. Doesn't hurt to check though * </p> * and generates warnings. * */ final class CheckSideEffects extends AbstractPostOrderCallback implements HotSwapCompilerPass { static final DiagnosticType USELESS_CODE_ERROR = DiagnosticType.warning("JSC_USELESS_CODE", "Suspicious code. {0}"); static final String PROTECTOR_FN = "JSCOMPILER_PRESERVE"; private final CheckLevel level; private final List<Node> problemNodes = Lists.newArrayList(); private final AbstractCompiler compiler; private final boolean protectSideEffectFreeCode; CheckSideEffects(AbstractCompiler compiler, CheckLevel level, boolean protectSideEffectFreeCode) { this.compiler = compiler; this.level = level; this.protectSideEffectFreeCode = protectSideEffectFreeCode; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); // Code with hidden side-effect code is common, for example // accessing "el.offsetWidth" forces a reflow in browsers, to allow this // will still allowing local dead code removal in general, // protect the "side-effect free" code in the source. // if (protectSideEffectFreeCode) { protectSideEffects(); } } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverse(compiler, scriptRoot, this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { // VOID nodes appear when there are extra semicolons at the BLOCK level. // I've been unable to think of any cases where this indicates a bug, // and apparently some people like keeping these semicolons around, // so we'll allow it. if (n.isEmpty() || n.isComma()) { return; } if (parent == null) { return; } int pt = parent.getType(); if (pt == Token.COMMA) { Node gramps = parent.getParent(); if (gramps.isCall() && parent == gramps.getFirstChild()) { // Semantically, a direct call to eval is different from an indirect // call to an eval. See Ecma-262 S15.1.2.1. So it's ok for the first // expression to a comma to be a no-op if it's used to indirect // an eval. if (n == parent.getFirstChild() && parent.getChildCount() == 2 && n.getNext().isName() && "eval".equals(n.getNext().getString())) { return; } } if (n == parent.getLastChild()) { for (Node an : parent.getAncestors()) { int ancestorType = an.getType(); if (ancestorType == Token.COMMA) continue; if (ancestorType != Token.EXPR_RESULT && ancestorType != Token.BLOCK) return; else break; } } } else if (pt != Token.EXPR_RESULT && pt != Token.BLOCK) { if (pt == Token.FOR && parent.getChildCount() == 4 && (n == parent.getFirstChild() || n == parent.getFirstChild().getNext().getNext())) { // Fall through and look for warnings for the 1st and 3rd child // of a for. } else { return; // it might be ok to not have a side-effect } } boolean isSimpleOp = NodeUtil.isSimpleOperatorType(n.getType()); if (isSimpleOp || !NodeUtil.mayHaveSideEffects(n, t.getCompiler())) { if (n.isQualifiedName() && n.getJSDocInfo() != null) { // This no-op statement was there so that JSDoc information could // be attached to the name. This check should not complain about it. return; } else if (n.isExprResult()) { // we already reported the problem when we visited the child. return; } String msg = "This code lacks side-effects. Is there a bug?"; if (n.isString()) { msg = "Is there a missing '+' on the previous line?"; } else if (isSimpleOp) { msg = "The result of the '" + Token.name(n.getType()).toLowerCase() + "' operator is not being used."; } t.getCompiler().report(t.makeError(n, level, USELESS_CODE_ERROR, msg)); // TODO(johnlenz): determine if it is necessary to // try to protect side-effect free statements as well. if (!NodeUtil.isStatement(n)) { problemNodes.add(n); } } } /** * Protect side-effect free nodes by making them parameters to a extern function call. This call * will be removed after all the optimizations passes have run. */ private void protectSideEffects() { if (!problemNodes.isEmpty()) { addExtern(); for (Node n : problemNodes) { Node name = IR.name(PROTECTOR_FN).srcref(n); name.putBooleanProp(Node.IS_CONSTANT_NAME, true); Node replacement = IR.call(name).srcref(n); replacement.putBooleanProp(Node.FREE_CALL, true); n.getParent().replaceChild(n, replacement); replacement.addChildToBack(n); } compiler.reportCodeChange(); } } private void addExtern() { Node name = IR.name(PROTECTOR_FN); name.putBooleanProp(Node.IS_CONSTANT_NAME, true); Node var = IR.var(name); // Add "@noalias" so we can strip the method when AliasExternals is enabled. JSDocInfoBuilder builder = new JSDocInfoBuilder(false); builder.recordNoAlias(); var.setJSDocInfo(builder.build(var)); CompilerInput input = compiler.getSynthesizedExternsInput(); input.getAstRoot(compiler).addChildrenToBack(var); compiler.reportCodeChange(); } /** Remove side-effect sync functions. */ static class StripProtection extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; StripProtection(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isCall()) { Node target = n.getFirstChild(); // TODO(johnlenz): add this to the coding convention // so we can remove goog.reflect.sinkValue as well. if (target.isName() && target.getString().equals(PROTECTOR_FN)) { Node expr = n.getLastChild(); n.detachChildren(); parent.replaceChild(n, expr); } } } } }
/** Peephole optimization to fold constants (e.g. x + 1 + 7 --> x + 8). */ class PeepholeFoldConstants extends AbstractPeepholeOptimization { // TODO(johnlenz): optimizations should not be emiting errors. Move these to // a check pass. static final DiagnosticType INVALID_GETELEM_INDEX_ERROR = DiagnosticType.warning("JSC_INVALID_GETELEM_INDEX_ERROR", "Array index not integer: {0}"); static final DiagnosticType INDEX_OUT_OF_BOUNDS_ERROR = DiagnosticType.warning("JSC_INDEX_OUT_OF_BOUNDS_ERROR", "Array index out of bounds: {0}"); static final DiagnosticType NEGATING_A_NON_NUMBER_ERROR = DiagnosticType.warning( "JSC_NEGATING_A_NON_NUMBER_ERROR", "Can''t negate non-numeric value: {0}"); static final DiagnosticType BITWISE_OPERAND_OUT_OF_RANGE = DiagnosticType.warning( "JSC_BITWISE_OPERAND_OUT_OF_RANGE", "Operand out of range, bitwise operation will lose information: {0}"); static final DiagnosticType SHIFT_AMOUNT_OUT_OF_BOUNDS = DiagnosticType.warning( "JSC_SHIFT_AMOUNT_OUT_OF_BOUNDS", "Shift amount out of bounds (see right operand): {0}"); static final DiagnosticType FRACTIONAL_BITWISE_OPERAND = DiagnosticType.warning("JSC_FRACTIONAL_BITWISE_OPERAND", "Fractional bitwise operand: {0}"); private static final double MAX_FOLD_NUMBER = Math.pow(2, 53); private final boolean late; private final boolean shouldUseTypes; /** * @param late When late is false, this mean we are currently running before most of the other * optimizations. In this case we would avoid optimizations that would make the code harder to * analyze. When this is true, we would do anything to minimize for size. */ PeepholeFoldConstants(boolean late, boolean shouldUseTypes) { this.late = late; this.shouldUseTypes = shouldUseTypes; } @Override Node optimizeSubtree(Node subtree) { switch (subtree.getType()) { case CALL: return tryFoldCall(subtree); case NEW: return tryFoldCtorCall(subtree); case TYPEOF: return tryFoldTypeof(subtree); case NOT: case POS: case NEG: case BITNOT: tryReduceOperandsForOp(subtree); return tryFoldUnaryOperator(subtree); case VOID: return tryReduceVoid(subtree); default: tryReduceOperandsForOp(subtree); return tryFoldBinaryOperator(subtree); } } private Node tryFoldBinaryOperator(Node subtree) { Node left = subtree.getFirstChild(); if (left == null) { return subtree; } Node right = left.getNext(); if (right == null) { return subtree; } // If we've reached here, node is truly a binary operator. switch (subtree.getType()) { case GETPROP: return tryFoldGetProp(subtree, left, right); case GETELEM: return tryFoldGetElem(subtree, left, right); case INSTANCEOF: return tryFoldInstanceof(subtree, left, right); case AND: case OR: return tryFoldAndOr(subtree, left, right); case LSH: case RSH: case URSH: return tryFoldShift(subtree, left, right); case ASSIGN: return tryFoldAssign(subtree, left, right); case ASSIGN_BITOR: case ASSIGN_BITXOR: case ASSIGN_BITAND: case ASSIGN_LSH: case ASSIGN_RSH: case ASSIGN_URSH: case ASSIGN_ADD: case ASSIGN_SUB: case ASSIGN_MUL: case ASSIGN_DIV: case ASSIGN_MOD: return tryUnfoldAssignOp(subtree, left, right); case ADD: return tryFoldAdd(subtree, left, right); case SUB: case DIV: case MOD: return tryFoldArithmeticOp(subtree, left, right); case MUL: case BITAND: case BITOR: case BITXOR: Node result = tryFoldArithmeticOp(subtree, left, right); if (result != subtree) { return result; } return tryFoldLeftChildOp(subtree, left, right); case LT: case GT: case LE: case GE: case EQ: case NE: case SHEQ: case SHNE: return tryFoldComparison(subtree, left, right); default: return subtree; } } private Node tryReduceVoid(Node n) { Node child = n.getFirstChild(); if ((!child.isNumber() || child.getDouble() != 0.0) && !mayHaveSideEffects(n)) { n.replaceChild(child, IR.number(0)); reportCodeChange(); } return n; } private void tryReduceOperandsForOp(Node n) { switch (n.getType()) { case ADD: Node left = n.getFirstChild(); Node right = n.getLastChild(); if (!NodeUtil.mayBeString(left, shouldUseTypes) && !NodeUtil.mayBeString(right, shouldUseTypes)) { tryConvertOperandsToNumber(n); } break; case ASSIGN_BITOR: case ASSIGN_BITXOR: case ASSIGN_BITAND: // TODO(johnlenz): convert these to integers. case ASSIGN_LSH: case ASSIGN_RSH: case ASSIGN_URSH: case ASSIGN_SUB: case ASSIGN_MUL: case ASSIGN_MOD: case ASSIGN_DIV: tryConvertToNumber(n.getLastChild()); break; case BITNOT: case BITOR: case BITXOR: case BITAND: case LSH: case RSH: case URSH: case SUB: case MUL: case MOD: case DIV: case POS: case NEG: tryConvertOperandsToNumber(n); break; } } private void tryConvertOperandsToNumber(Node n) { Node next; for (Node c = n.getFirstChild(); c != null; c = next) { next = c.getNext(); tryConvertToNumber(c); } } private void tryConvertToNumber(Node n) { switch (n.getType()) { case NUMBER: // Nothing to do return; case AND: case OR: case COMMA: tryConvertToNumber(n.getLastChild()); return; case HOOK: tryConvertToNumber(n.getSecondChild()); tryConvertToNumber(n.getLastChild()); return; case NAME: if (!NodeUtil.isUndefined(n)) { return; } break; } Double result = NodeUtil.getNumberValue(n, shouldUseTypes); if (result == null) { return; } double value = result; Node replacement = NodeUtil.numberNode(value, n); if (replacement.isEquivalentTo(n)) { return; } n.getParent().replaceChild(n, replacement); reportCodeChange(); } /** * Folds 'typeof(foo)' if foo is a literal, e.g. typeof("bar") --> "string" typeof(6) --> "number" */ private Node tryFoldTypeof(Node originalTypeofNode) { Preconditions.checkArgument(originalTypeofNode.isTypeOf()); Node argumentNode = originalTypeofNode.getFirstChild(); if (argumentNode == null || !NodeUtil.isLiteralValue(argumentNode, true)) { return originalTypeofNode; } String typeNameString = null; switch (argumentNode.getType()) { case FUNCTION: typeNameString = "function"; break; case STRING: typeNameString = "string"; break; case NUMBER: typeNameString = "number"; break; case TRUE: case FALSE: typeNameString = "boolean"; break; case NULL: case OBJECTLIT: case ARRAYLIT: typeNameString = "object"; break; case VOID: typeNameString = "undefined"; break; case NAME: // We assume here that programs don't change the value of the // keyword undefined to something other than the value undefined. if ("undefined".equals(argumentNode.getString())) { typeNameString = "undefined"; } break; } if (typeNameString != null) { Node newNode = IR.string(typeNameString); originalTypeofNode.getParent().replaceChild(originalTypeofNode, newNode); reportCodeChange(); return newNode; } return originalTypeofNode; } private Node tryFoldUnaryOperator(Node n) { Preconditions.checkState(n.hasOneChild(), n); Node left = n.getFirstChild(); Node parent = n.getParent(); if (left == null) { return n; } TernaryValue leftVal = NodeUtil.getPureBooleanValue(left); if (leftVal == TernaryValue.UNKNOWN) { return n; } switch (n.getType()) { case NOT: // Don't fold !0 and !1 back to false. if (late && left.isNumber()) { double numValue = left.getDouble(); if (numValue == 0 || numValue == 1) { return n; } } Node replacementNode = NodeUtil.booleanNode(!leftVal.toBoolean(true)); parent.replaceChild(n, replacementNode); reportCodeChange(); return replacementNode; case POS: if (NodeUtil.isNumericResult(left)) { // POS does nothing to numeric values. parent.replaceChild(n, left.detachFromParent()); reportCodeChange(); return left; } return n; case NEG: if (left.isName()) { if (left.getString().equals("Infinity")) { // "-Infinity" is valid and a literal, don't modify it. return n; } else if (left.getString().equals("NaN")) { // "-NaN" is "NaN". n.removeChild(left); parent.replaceChild(n, left); reportCodeChange(); return left; } } if (left.isNumber()) { double negNum = -left.getDouble(); Node negNumNode = IR.number(negNum); parent.replaceChild(n, negNumNode); reportCodeChange(); return negNumNode; } else { // left is not a number node, so do not replace, but warn the // user because they can't be doing anything good report(NEGATING_A_NON_NUMBER_ERROR, left); return n; } case BITNOT: try { double val = left.getDouble(); if (val >= Integer.MIN_VALUE && val <= Integer.MAX_VALUE) { int intVal = (int) val; if (intVal == val) { Node notIntValNode = IR.number(~intVal); parent.replaceChild(n, notIntValNode); reportCodeChange(); return notIntValNode; } else { report(FRACTIONAL_BITWISE_OPERAND, left); return n; } } else { report(BITWISE_OPERAND_OUT_OF_RANGE, left); return n; } } catch (UnsupportedOperationException ex) { // left is not a number node, so do not replace, but warn the // user because they can't be doing anything good report(NEGATING_A_NON_NUMBER_ERROR, left); return n; } default: return n; } } /** Try to fold {@code left instanceof right} into {@code true} or {@code false}. */ private Node tryFoldInstanceof(Node n, Node left, Node right) { Preconditions.checkArgument(n.isInstanceOf()); // TODO(johnlenz) Use type information if available to fold // instanceof. if (NodeUtil.isLiteralValue(left, true) && !mayHaveSideEffects(right)) { Node replacementNode = null; if (NodeUtil.isImmutableValue(left)) { // Non-object types are never instances. replacementNode = IR.falseNode(); } else if (right.isName() && "Object".equals(right.getString())) { replacementNode = IR.trueNode(); } if (replacementNode != null) { n.getParent().replaceChild(n, replacementNode); reportCodeChange(); return replacementNode; } } return n; } private Node tryFoldAssign(Node n, Node left, Node right) { Preconditions.checkArgument(n.isAssign()); if (!late) { return n; } // Tries to convert x = x + y -> x += y; if (!right.hasChildren() || right.getSecondChild() != right.getLastChild()) { // RHS must have two children. return n; } if (mayHaveSideEffects(left)) { return n; } Node newRight; if (areNodesEqualForInlining(left, right.getFirstChild())) { newRight = right.getLastChild(); } else if (NodeUtil.isCommutative(right.getType()) && areNodesEqualForInlining(left, right.getLastChild())) { newRight = right.getFirstChild(); } else { return n; } Token newType = null; switch (right.getType()) { case ADD: newType = Token.ASSIGN_ADD; break; case BITAND: newType = Token.ASSIGN_BITAND; break; case BITOR: newType = Token.ASSIGN_BITOR; break; case BITXOR: newType = Token.ASSIGN_BITXOR; break; case DIV: newType = Token.ASSIGN_DIV; break; case LSH: newType = Token.ASSIGN_LSH; break; case MOD: newType = Token.ASSIGN_MOD; break; case MUL: newType = Token.ASSIGN_MUL; break; case RSH: newType = Token.ASSIGN_RSH; break; case SUB: newType = Token.ASSIGN_SUB; break; case URSH: newType = Token.ASSIGN_URSH; break; default: return n; } Node newNode = new Node(newType, left.detachFromParent(), newRight.detachFromParent()); n.getParent().replaceChild(n, newNode); reportCodeChange(); return newNode; } private Node tryUnfoldAssignOp(Node n, Node left, Node right) { if (late) { return n; } if (!n.hasChildren() || n.getSecondChild() != n.getLastChild()) { return n; } if (mayHaveSideEffects(left)) { return n; } // Tries to convert x += y -> x = x + y; Token op = NodeUtil.getOpFromAssignmentOp(n); Node replacement = IR.assign( left.detachFromParent(), new Node(op, left.cloneTree(), right.detachFromParent()).srcref(n)); n.getParent().replaceChild(n, replacement); reportCodeChange(); return replacement; } /** Try to fold a AND/OR node. */ private Node tryFoldAndOr(Node n, Node left, Node right) { Node parent = n.getParent(); Node result = null; Token type = n.getType(); TernaryValue leftVal = NodeUtil.getImpureBooleanValue(left); if (leftVal != TernaryValue.UNKNOWN) { boolean lval = leftVal.toBoolean(true); // (TRUE || x) => TRUE (also, (3 || x) => 3) // (FALSE && x) => FALSE if (lval && type == Token.OR || !lval && type == Token.AND) { result = left; } else if (!mayHaveSideEffects(left)) { // (FALSE || x) => x // (TRUE && x) => x result = right; } else { // Left side may have side effects, but we know its boolean value. // e.g. true_with_sideeffects || foo() => true_with_sideeffects, foo() // or: false_with_sideeffects && foo() => false_with_sideeffects, foo() // This, combined with PeepholeRemoveDeadCode, helps reduce expressions // like "x() || false || z()". n.detachChildren(); result = IR.comma(left, right); } } // Note: Right hand side folding is handled by // PeepholeMinimizeConditions#tryMinimizeCondition if (result != null) { // Fold it! n.detachChildren(); parent.replaceChild(n, result); reportCodeChange(); return result; } else { return n; } } /** * Expressions such as [foo() + 'a' + 'b'] generate parse trees where no node has two const * children ((foo() + 'a') + 'b'), so tryFoldAdd() won't fold it -- tryFoldLeftChildAdd() will * (for Strings). Specifically, it folds Add expressions where: - The left child is also and add * expression - The right child is a constant value - The left child's right child is a STRING * constant. */ private Node tryFoldChildAddString(Node n, Node left, Node right) { if (NodeUtil.isLiteralValue(right, false) && left.isAdd()) { Node ll = left.getFirstChild(); Node lr = ll.getNext(); // Left's right child MUST be a string. We would not want to fold // foo() + 2 + 'a' because we don't know what foo() will return, and // therefore we don't know if left is a string concat, or a numeric add. if (lr.isString()) { String leftString = NodeUtil.getStringValue(lr); String rightString = NodeUtil.getStringValue(right); if (leftString != null && rightString != null) { left.removeChild(ll); String result = leftString + rightString; n.replaceChild(left, ll); n.replaceChild(right, IR.string(result)); reportCodeChange(); return n; } } } if (NodeUtil.isLiteralValue(left, false) && right.isAdd()) { Node rl = right.getFirstChild(); Node rr = right.getLastChild(); // Left's right child MUST be a string. We would not want to fold // foo() + 2 + 'a' because we don't know what foo() will return, and // therefore we don't know if left is a string concat, or a numeric add. if (rl.isString()) { String leftString = NodeUtil.getStringValue(left); String rightString = NodeUtil.getStringValue(rl); if (leftString != null && rightString != null) { right.removeChild(rr); String result = leftString + rightString; n.replaceChild(right, rr); n.replaceChild(left, IR.string(result)); reportCodeChange(); return n; } } } return n; } /** Try to fold an ADD node with constant operands */ private Node tryFoldAddConstantString(Node n, Node left, Node right) { if (left.isString() || right.isString() || left.isArrayLit() || right.isArrayLit()) { // Add strings. String leftString = NodeUtil.getStringValue(left); String rightString = NodeUtil.getStringValue(right); if (leftString != null && rightString != null) { Node newStringNode = IR.string(leftString + rightString); n.getParent().replaceChild(n, newStringNode); reportCodeChange(); return newStringNode; } } return n; } /** Try to fold arithmetic binary operators */ private Node tryFoldArithmeticOp(Node n, Node left, Node right) { Node result = performArithmeticOp(n.getType(), left, right); if (result != null) { result.useSourceInfoIfMissingFromForTree(n); n.getParent().replaceChild(n, result); reportCodeChange(); return result; } return n; } /** Try to fold arithmetic binary operators */ private Node performArithmeticOp(Token opType, Node left, Node right) { // Unlike other operations, ADD operands are not always converted // to Number. if (opType == Token.ADD && (NodeUtil.mayBeString(left, shouldUseTypes) || NodeUtil.mayBeString(right, shouldUseTypes))) { return null; } double result; // TODO(johnlenz): Handle NaN with unknown value. BIT ops convert NaN // to zero so this is a little awkward here. Double lValObj = NodeUtil.getNumberValue(left, shouldUseTypes); if (lValObj == null) { return null; } Double rValObj = NodeUtil.getNumberValue(right, shouldUseTypes); if (rValObj == null) { return null; } double lval = lValObj; double rval = rValObj; switch (opType) { case BITAND: result = NodeUtil.toInt32(lval) & NodeUtil.toInt32(rval); break; case BITOR: result = NodeUtil.toInt32(lval) | NodeUtil.toInt32(rval); break; case BITXOR: result = NodeUtil.toInt32(lval) ^ NodeUtil.toInt32(rval); break; case ADD: result = lval + rval; break; case SUB: result = lval - rval; break; case MUL: result = lval * rval; break; case MOD: if (rval == 0) { return null; } result = lval % rval; break; case DIV: if (rval == 0) { return null; } result = lval / rval; break; default: throw new Error("Unexpected arithmetic operator"); } // TODO(johnlenz): consider removing the result length check. // length of the left and right value plus 1 byte for the operator. if ((String.valueOf(result).length() <= String.valueOf(lval).length() + String.valueOf(rval).length() + 1 // Do not try to fold arithmetic for numbers > 2^53. After that // point, fixed-point math starts to break down and become inaccurate. && Math.abs(result) <= MAX_FOLD_NUMBER) || Double.isNaN(result) || result == Double.POSITIVE_INFINITY || result == Double.NEGATIVE_INFINITY) { return NodeUtil.numberNode(result, null); } return null; } /** * Expressions such as [foo() * 10 * 20] generate parse trees where no node has two const children * ((foo() * 10) * 20), so performArithmeticOp() won't fold it -- tryFoldLeftChildOp() will. * Specifically, it folds associative expressions where: - The left child is also an associative * expression of the same time. - The right child is a constant NUMBER constant. - The left * child's right child is a NUMBER constant. */ private Node tryFoldLeftChildOp(Node n, Node left, Node right) { Token opType = n.getType(); Preconditions.checkState( (NodeUtil.isAssociative(opType) && NodeUtil.isCommutative(opType)) || n.isAdd()); Preconditions.checkState(!n.isAdd() || !NodeUtil.mayBeString(n, shouldUseTypes)); // Use getNumberValue to handle constants like "NaN" and "Infinity" // other values are converted to numbers elsewhere. Double rightValObj = NodeUtil.getNumberValue(right, shouldUseTypes); if (rightValObj != null && left.getType() == opType) { Preconditions.checkState(left.getChildCount() == 2); Node ll = left.getFirstChild(); Node lr = ll.getNext(); Node valueToCombine = ll; Node replacement = performArithmeticOp(opType, valueToCombine, right); if (replacement == null) { valueToCombine = lr; replacement = performArithmeticOp(opType, valueToCombine, right); } if (replacement != null) { // Remove the child that has been combined left.removeChild(valueToCombine); // Replace the left op with the remaining child. n.replaceChild(left, left.removeFirstChild()); // New "-Infinity" node need location info explicitly // added. replacement.useSourceInfoIfMissingFromForTree(right); n.replaceChild(right, replacement); reportCodeChange(); } } return n; } private Node tryFoldAdd(Node node, Node left, Node right) { Preconditions.checkArgument(node.isAdd()); if (NodeUtil.mayBeString(node, shouldUseTypes)) { if (NodeUtil.isLiteralValue(left, false) && NodeUtil.isLiteralValue(right, false)) { // '6' + 7 return tryFoldAddConstantString(node, left, right); } else { // a + 7 or 6 + a return tryFoldChildAddString(node, left, right); } } else { // Try arithmetic add Node result = tryFoldArithmeticOp(node, left, right); if (result != node) { return result; } return tryFoldLeftChildOp(node, left, right); } } /** Try to fold shift operations */ private Node tryFoldShift(Node n, Node left, Node right) { if (left.isNumber() && right.isNumber()) { double result; double lval = left.getDouble(); double rval = right.getDouble(); // check ranges. We do not do anything that would clip the double to // a 32-bit range, since the user likely does not intend that. if (lval < Integer.MIN_VALUE) { report(BITWISE_OPERAND_OUT_OF_RANGE, left); return n; } // only the lower 5 bits are used when shifting, so don't do anything // if the shift amount is outside [0,32) if (!(rval >= 0 && rval < 32)) { report(SHIFT_AMOUNT_OUT_OF_BOUNDS, n); return n; } int rvalInt = (int) rval; if (rvalInt != rval) { report(FRACTIONAL_BITWISE_OPERAND, right); return n; } switch (n.getType()) { case LSH: case RSH: // Convert the numbers to ints if (lval > Integer.MAX_VALUE) { report(BITWISE_OPERAND_OUT_OF_RANGE, left); return n; } int lvalInt = (int) lval; if (lvalInt != lval) { report(FRACTIONAL_BITWISE_OPERAND, left); return n; } if (n.getType() == Token.LSH) { result = lvalInt << rvalInt; } else { result = lvalInt >> rvalInt; } break; case URSH: // JavaScript handles zero shifts on signed numbers differently than // Java as an Java int can not represent the unsigned 32-bit number // where JavaScript can so use a long here. long maxUint32 = 0xffffffffL; if (lval > maxUint32) { report(BITWISE_OPERAND_OUT_OF_RANGE, left); return n; } long lvalLong = (long) lval; if (lvalLong != lval) { report(FRACTIONAL_BITWISE_OPERAND, left); return n; } result = (lvalLong & maxUint32) >>> rvalInt; break; default: throw new AssertionError("Unknown shift operator: " + n.getType()); } Node newNumber = IR.number(result); n.getParent().replaceChild(n, newNumber); reportCodeChange(); return newNumber; } return n; } /** Try to fold comparison nodes, e.g == */ private Node tryFoldComparison(Node n, Node left, Node right) { TernaryValue result = evaluateComparison(n.getType(), left, right, shouldUseTypes); if (result == TernaryValue.UNKNOWN) { return n; } Node newNode = NodeUtil.booleanNode(result.toBoolean(true)); n.getParent().replaceChild(n, newNode); reportCodeChange(); return newNode; } /** http://www.ecma-international.org/ecma-262/6.0/#sec-abstract-relational-comparison */ private static TernaryValue tryAbstractRelationalComparison( Node left, Node right, boolean useTypes, boolean willNegate) { // First, try to evaluate based on the general type. ValueType leftValueType = NodeUtil.getKnownValueType(left); ValueType rightValueType = NodeUtil.getKnownValueType(right); if (leftValueType != ValueType.UNDETERMINED && rightValueType != ValueType.UNDETERMINED) { if (leftValueType == ValueType.STRING && rightValueType == ValueType.STRING) { String lv = NodeUtil.getStringValue(left); String rv = NodeUtil.getStringValue(right); if (lv != null && rv != null) { // In JS, browsers parse \v differently. So do not compare strings if one contains \v. if (lv.indexOf('\u000B') != -1 || rv.indexOf('\u000B') != -1) { return TernaryValue.UNKNOWN; } else { return TernaryValue.forBoolean(lv.compareTo(rv) < 0); } } else if (left.isTypeOf() && right.isTypeOf() && left.getFirstChild().isName() && right.getFirstChild().isName() && left.getFirstChild().getString().equals(right.getFirstChild().getString())) { // Special case: `typeof a < typeof a` is always false. return TernaryValue.FALSE; } } } // Then, try to evaluate based on the value of the node. Try comparing as numbers. Double lv = NodeUtil.getNumberValue(left, useTypes); Double rv = NodeUtil.getNumberValue(right, useTypes); if (lv == null || rv == null) { // Special case: `x < x` is always false. // // TODO(moz): If we knew the named value wouldn't be NaN, it would be nice to handle // LE and GE. We should use type information if available here. if (!willNegate && left.isName() && right.isName()) { if (left.getString().equals(right.getString())) { return TernaryValue.FALSE; } } return TernaryValue.UNKNOWN; } if (Double.isNaN(lv) || Double.isNaN(rv)) { return TernaryValue.forBoolean(willNegate); } else { return TernaryValue.forBoolean(lv.doubleValue() < rv.doubleValue()); } } /** http://www.ecma-international.org/ecma-262/6.0/#sec-abstract-equality-comparison */ private static TernaryValue tryAbstractEqualityComparison( Node left, Node right, boolean useTypes) { // Evaluate based on the general type. ValueType leftValueType = NodeUtil.getKnownValueType(left); ValueType rightValueType = NodeUtil.getKnownValueType(right); if (leftValueType != ValueType.UNDETERMINED && rightValueType != ValueType.UNDETERMINED) { // Delegate to strict equality comparison for values of the same type. if (leftValueType == rightValueType) { return tryStrictEqualityComparison(left, right, useTypes); } if ((leftValueType == ValueType.NULL && rightValueType == ValueType.VOID) || (leftValueType == ValueType.VOID && rightValueType == ValueType.NULL)) { return TernaryValue.TRUE; } if ((leftValueType == ValueType.NUMBER && rightValueType == ValueType.STRING) || rightValueType == ValueType.BOOLEAN) { Double rv = NodeUtil.getNumberValue(right, useTypes); return rv == null ? TernaryValue.UNKNOWN : tryAbstractEqualityComparison(left, IR.number(rv), useTypes); } if ((leftValueType == ValueType.STRING && rightValueType == ValueType.NUMBER) || leftValueType == ValueType.BOOLEAN) { Double lv = NodeUtil.getNumberValue(left, useTypes); return lv == null ? TernaryValue.UNKNOWN : tryAbstractEqualityComparison(IR.number(lv), right, useTypes); } if ((leftValueType == ValueType.STRING || leftValueType == ValueType.NUMBER) && rightValueType == ValueType.OBJECT) { return TernaryValue.UNKNOWN; } if (leftValueType == ValueType.OBJECT && (rightValueType == ValueType.STRING || rightValueType == ValueType.NUMBER)) { return TernaryValue.UNKNOWN; } return TernaryValue.FALSE; } // In general, the rest of the cases cannot be folded. return TernaryValue.UNKNOWN; } /** http://www.ecma-international.org/ecma-262/6.0/#sec-strict-equality-comparison */ private static TernaryValue tryStrictEqualityComparison(Node left, Node right, boolean useTypes) { // First, try to evaluate based on the general type. ValueType leftValueType = NodeUtil.getKnownValueType(left); ValueType rightValueType = NodeUtil.getKnownValueType(right); if (leftValueType != ValueType.UNDETERMINED && rightValueType != ValueType.UNDETERMINED) { // Strict equality can only be true for values of the same type. if (leftValueType != rightValueType) { return TernaryValue.FALSE; } switch (leftValueType) { case VOID: case NULL: return TernaryValue.TRUE; case NUMBER: { if (NodeUtil.isNaN(left)) { return TernaryValue.FALSE; } if (NodeUtil.isNaN(right)) { return TernaryValue.FALSE; } Double lv = NodeUtil.getNumberValue(left, useTypes); Double rv = NodeUtil.getNumberValue(right, useTypes); if (lv != null && rv != null) { return TernaryValue.forBoolean(lv.doubleValue() == rv.doubleValue()); } break; } case STRING: { String lv = NodeUtil.getStringValue(left); String rv = NodeUtil.getStringValue(right); if (lv != null && rv != null) { // In JS, browsers parse \v differently. So do not consider strings // equal if one contains \v. if (lv.indexOf('\u000B') != -1 || rv.indexOf('\u000B') != -1) { return TernaryValue.UNKNOWN; } else { return lv.equals(rv) ? TernaryValue.TRUE : TernaryValue.FALSE; } } else if (left.isTypeOf() && right.isTypeOf() && left.getFirstChild().isName() && right.getFirstChild().isName() && left.getFirstChild().getString().equals(right.getFirstChild().getString())) { // Special case, typeof a == typeof a is always true. return TernaryValue.TRUE; } break; } case BOOLEAN: { TernaryValue lv = NodeUtil.getPureBooleanValue(left); TernaryValue rv = NodeUtil.getPureBooleanValue(right); return lv.and(rv).or(lv.not().and(rv.not())); } default: // Symbol and Object cannot be folded in the general case. return TernaryValue.UNKNOWN; } } // Then, try to evaluate based on the value of the node. There's only one special case: // Any strict equality comparison against NaN returns false. if (NodeUtil.isNaN(left) || NodeUtil.isNaN(right)) { return TernaryValue.FALSE; } return TernaryValue.UNKNOWN; } static TernaryValue evaluateComparison(Token op, Node left, Node right, boolean useTypes) { // Don't try to minimize side-effects here. if (NodeUtil.mayHaveSideEffects(left) || NodeUtil.mayHaveSideEffects(right)) { return TernaryValue.UNKNOWN; } switch (op) { case EQ: return tryAbstractEqualityComparison(left, right, useTypes); case NE: return tryAbstractEqualityComparison(left, right, useTypes).not(); case SHEQ: return tryStrictEqualityComparison(left, right, useTypes); case SHNE: return tryStrictEqualityComparison(left, right, useTypes).not(); case LT: return tryAbstractRelationalComparison(left, right, useTypes, false); case GT: return tryAbstractRelationalComparison(right, left, useTypes, false); case LE: return tryAbstractRelationalComparison(right, left, useTypes, true).not(); case GE: return tryAbstractRelationalComparison(left, right, useTypes, true).not(); } throw new IllegalStateException("Unexpected operator for comparison"); } /** * Try to fold away unnecessary object instantiation. e.g. this[new String('eval')] -> this.eval */ private Node tryFoldCtorCall(Node n) { Preconditions.checkArgument(n.isNew()); // we can remove this for GETELEM calls (anywhere else?) if (inForcedStringContext(n)) { return tryFoldInForcedStringContext(n); } return n; } /** Remove useless calls: Object.defineProperties(o, {}) -> o */ private Node tryFoldCall(Node n) { Preconditions.checkArgument(n.isCall()); if (NodeUtil.isObjectDefinePropertiesDefinition(n)) { Node srcObj = n.getLastChild(); if (srcObj.isObjectLit() && !srcObj.hasChildren()) { Node parent = n.getParent(); Node destObj = n.getSecondChild().detachFromParent(); parent.replaceChild(n, destObj); reportCodeChange(); } } return n; } /** Returns whether this node must be coerced to a string. */ private static boolean inForcedStringContext(Node n) { if (n.getParent().isGetElem() && n.getParent().getLastChild() == n) { return true; } // we can fold in the case "" + new String("") return n.getParent().isAdd(); } private Node tryFoldInForcedStringContext(Node n) { // For now, we only know how to fold ctors. Preconditions.checkArgument(n.isNew()); Node objectType = n.getFirstChild(); if (!objectType.isName()) { return n; } if (objectType.getString().equals("String")) { Node value = objectType.getNext(); String stringValue = null; if (value == null) { stringValue = ""; } else { if (!NodeUtil.isImmutableValue(value)) { return n; } stringValue = NodeUtil.getStringValue(value); } if (stringValue == null) { return n; } Node parent = n.getParent(); Node newString = IR.string(stringValue); parent.replaceChild(n, newString); newString.useSourceInfoIfMissingFrom(parent); reportCodeChange(); return newString; } return n; } /** Try to fold array-element. e.g [1, 2, 3][10]; */ private Node tryFoldGetElem(Node n, Node left, Node right) { Preconditions.checkArgument(n.isGetElem()); if (left.isObjectLit()) { return tryFoldObjectPropAccess(n, left, right); } if (left.isArrayLit()) { return tryFoldArrayAccess(n, left, right); } if (left.isString()) { return tryFoldStringArrayAccess(n, left, right); } return n; } /** Try to fold array-length. e.g [1, 2, 3].length ==> 3, [x, y].length ==> 2 */ private Node tryFoldGetProp(Node n, Node left, Node right) { Preconditions.checkArgument(n.isGetProp()); if (left.isObjectLit()) { return tryFoldObjectPropAccess(n, left, right); } if (right.isString() && right.getString().equals("length")) { int knownLength = -1; switch (left.getType()) { case ARRAYLIT: if (mayHaveSideEffects(left)) { // Nope, can't fold this, without handling the side-effects. return n; } knownLength = left.getChildCount(); break; case STRING: knownLength = left.getString().length(); break; default: // Not a foldable case, forget it. return n; } Preconditions.checkState(knownLength != -1); Node lengthNode = IR.number(knownLength); n.getParent().replaceChild(n, lengthNode); reportCodeChange(); return lengthNode; } return n; } private Node tryFoldArrayAccess(Node n, Node left, Node right) { // If GETPROP/GETELEM is used as assignment target the array literal is // acting as a temporary we can't fold it here: // "[][0] += 1" if (NodeUtil.isAssignmentTarget(n)) { return n; } if (!right.isNumber()) { // Sometimes people like to use complex expressions to index into // arrays, or strings to index into array methods. return n; } double index = right.getDouble(); int intIndex = (int) index; if (intIndex != index) { report(INVALID_GETELEM_INDEX_ERROR, right); return n; } if (intIndex < 0) { report(INDEX_OUT_OF_BOUNDS_ERROR, right); return n; } Node current = left.getFirstChild(); Node elem = null; for (int i = 0; current != null; i++) { if (i != intIndex) { if (mayHaveSideEffects(current)) { return n; } } else { elem = current; } current = current.getNext(); } if (elem == null) { report(INDEX_OUT_OF_BOUNDS_ERROR, right); return n; } if (elem.isEmpty()) { elem = NodeUtil.newUndefinedNode(elem); } else { left.removeChild(elem); } // Replace the entire GETELEM with the value n.getParent().replaceChild(n, elem); reportCodeChange(); return elem; } private Node tryFoldStringArrayAccess(Node n, Node left, Node right) { // If GETPROP/GETELEM is used as assignment target the array literal is // acting as a temporary we can't fold it here: // "[][0] += 1" if (NodeUtil.isAssignmentTarget(n)) { return n; } if (!right.isNumber()) { // Sometimes people like to use complex expressions to index into // arrays, or strings to index into array methods. return n; } double index = right.getDouble(); int intIndex = (int) index; if (intIndex != index) { report(INVALID_GETELEM_INDEX_ERROR, right); return n; } if (intIndex < 0) { report(INDEX_OUT_OF_BOUNDS_ERROR, right); return n; } Preconditions.checkState(left.isString()); String value = left.getString(); if (intIndex >= value.length()) { report(INDEX_OUT_OF_BOUNDS_ERROR, right); return n; } char c = 0; // Note: For now skip the strings with unicode // characters as I don't understand the differences // between Java and JavaScript. for (int i = 0; i <= intIndex; i++) { c = value.charAt(i); if (c < 32 || c > 127) { return n; } } Node elem = IR.string(Character.toString(c)); // Replace the entire GETELEM with the value n.getParent().replaceChild(n, elem); reportCodeChange(); return elem; } private Node tryFoldObjectPropAccess(Node n, Node left, Node right) { Preconditions.checkArgument(NodeUtil.isGet(n)); if (!left.isObjectLit() || !right.isString()) { return n; } if (NodeUtil.isAssignmentTarget(n)) { // If GETPROP/GETELEM is used as assignment target the object literal is // acting as a temporary we can't fold it here: // "{a:x}.a += 1" is not "x += 1" return n; } // find the last definition in the object literal Node key = null; Node value = null; for (Node c = left.getFirstChild(); c != null; c = c.getNext()) { if (c.getString().equals(right.getString())) { switch (c.getType()) { case SETTER_DEF: continue; case GETTER_DEF: case STRING_KEY: if (value != null && mayHaveSideEffects(value)) { // The previously found value had side-effects return n; } key = c; value = key.getFirstChild(); break; default: throw new IllegalStateException(); } } else if (mayHaveSideEffects(c.getFirstChild())) { // We don't handle the side-effects here as they might need a temporary // or need to be reordered. return n; } } // Didn't find a definition of the name in the object literal, it might // be coming from the Object prototype if (value == null) { return n; } if (value.isFunction() && NodeUtil.referencesThis(value)) { // 'this' may refer to the object we are trying to remove return n; } Node replacement = value.detachFromParent(); if (key.isGetterDef()) { replacement = IR.call(replacement); replacement.putBooleanProp(Node.FREE_CALL, true); } n.getParent().replaceChild(n, replacement); reportCodeChange(); return n; } }
/** * 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)); } } }
/** 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); } }
/** * 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 "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); } }
/** Rewrites all the library polyfills. */ public class RewritePolyfills implements HotSwapCompilerPass { static final DiagnosticType INSUFFICIENT_OUTPUT_VERSION_ERROR = DiagnosticType.warning( "JSC_INSUFFICIENT_OUTPUT_VERSION", "Built-in ''{0}'' not supported in output version {1}: set --language_out to at least {2}"); // Also polyfill references to e.g. goog.global.Map or window.Map. private static final String GLOBAL = "goog.global."; private static final String WINDOW = "window."; /** * Represents a single polyfill: specifically, a native symbol (either a qualified name or a * property name) that can be rewritten and/or installed to provide the functionality to a lower * version. This is a simple value type. */ private static class Polyfill { /** * The language version at (or above) which the native symbol is available and sufficient. If * the language out flag is at least as high as {@code nativeVersion} then no rewriting will * happen. */ final FeatureSet nativeVersion; /** * The required language version for the polyfill to work. This should not be higher than {@code * nativeVersion}, but may be the same in cases where there is no polyfill provided. This is * used to emit a warning if the language out flag is too low. */ final FeatureSet polyfillVersion; /** * Optional qualified name to drop-in replace for the native symbol. May be empty if no direct * rewriting is to take place. */ final String rewrite; /** * Optional "installer" to insert (once) at the top of a source file. If present, this should be * a JavaScript statement, or empty if no installer should be inserted. */ final String installer; Polyfill( FeatureSet nativeVersion, FeatureSet polyfillVersion, String rewrite, String installer) { this.nativeVersion = nativeVersion; this.polyfillVersion = polyfillVersion; this.rewrite = rewrite; this.installer = installer; } } /** * Describes all the available polyfills, including native and required versions, and how to use * them. */ static class Polyfills { // Map of method polyfills, keyed by native method name. private final ImmutableMultimap<String, Polyfill> methods; // Map of static polyfills, keyed by fully-qualified native name. private final ImmutableMap<String, Polyfill> statics; private Polyfills(Builder builder) { this.methods = builder.methodsBuilder.build(); this.statics = builder.staticsBuilder.build(); } /** * Provides a DSL for building a {@link Polyfills} object by calling {@link #addStatics}, {@link * #addMethods}, and {@link #addClasses} to register the various polyfills and provide * information about the native and polyfilled versions, and how to use the polyfills. */ static class Builder { private final ImmutableMultimap.Builder<String, Polyfill> methodsBuilder = ImmutableMultimap.builder(); private final ImmutableMap.Builder<String, Polyfill> staticsBuilder = ImmutableMap.builder(); /** * Registers one or more prototype method in a single namespace. The pass is agnostic with * regard to the class whose prototype is being augmented. The {@code base} parameter * specifies the qualified namespace where all the {@code methods} reside. Each method is * expected to have a sibling named with the {@code $install} suffix. The method calls * themselves are not rewritten, but whenever one is detected, its installer(s) will be added * to the top of the source file whenever the output version is less than {@code * nativeVersion}. For example, defining {@code addMethods(ES6, ES5, "$jscomp.string", * "startsWith", "endsWith")} will cause {@code $jscomp.string.startsWith$install();} to be * added to any source file that calls, e.g. {@code foo.startsWith}. * * <p>If {@code base} is blank, then no polyfills will be installed. This is useful for * documenting unimplemented polyfills. */ Builder addMethods( FeatureSet nativeVersion, FeatureSet polyfillVersion, String base, String... methods) { if (!base.isEmpty()) { for (String method : methods) { methodsBuilder.put( method, new Polyfill( nativeVersion, polyfillVersion, "", base + "." + method + "$install();")); } } // TODO(sdh): If base.isEmpty() then it means no polyfill is implemented. Is there // any way we can warn if the output language is too low? It's not likely, since // there's no good way to determine if it's actually intended as an ES6 method or // else is defined elsewhere. return this; } /** * Registers one or more static rewrite polyfill, which is a simple rewrite of one qualified * name to another. For each {@code name} in {@code statics}, {@code nativeBase + '.' + name} * will be replaced with {@code polyfillBase + '.' + name} whenever the output version is less * than {@code nativeVersion}. For eaxmple, defining {@code addStatics(ES6, ES5, * "$jscomp.math", "Math", "clz32", "imul")} will cause {@code Math.clz32} to be rewritten as * {@code $jscomp.math.clz32}. * * <p>If {@code polyfillBase} is blank, then no polyfills will be installed. This is useful * for documenting unimplemented polyfills, and will trigger a warning if the language output * mode is less than the native version. */ Builder addStatics( FeatureSet nativeVersion, FeatureSet polyfillVersion, String polyfillBase, String nativeBase, String... statics) { for (String item : statics) { String nativeName = nativeBase + "." + item; String polyfillName = !polyfillBase.isEmpty() ? polyfillBase + "." + item : ""; Polyfill polyfill = new Polyfill(nativeVersion, polyfillVersion, polyfillName, ""); staticsBuilder.put(nativeName, polyfill); staticsBuilder.put(GLOBAL + nativeName, polyfill); staticsBuilder.put(WINDOW + nativeName, polyfill); } return this; } /** * Registers one or more class polyfill. Class polyfills are both rewritten in place and also * installed (so that faster native versions may be preferred if available). The {@code base} * parameter is a qualified name prefix added to the class name to get the polyfill's name. A * sibling method with the {@code $install} suffix should also be present. For example, * defining {@code addClasses(ES6, ES5, "$jscomp", "Map", "Set")} will cause {@code new Map()} * to be rewritten as {@code new $jscomp.Map()} and will insert {@code $jscomp.Map$install();} * at the top of the source file whenever the output version is less than {@code * nativeVersion}. * * <p>If {@code base} is blank, then no polyfills will be installed. This is useful for * documenting unimplemented polyfills, and will trigger a warning if the language output mode * is less than the native version. */ Builder addClasses( FeatureSet nativeVersion, FeatureSet polyfillVersion, String base, String... classes) { for (String className : classes) { String polyfillName = base + "." + className; Polyfill polyfill = !base.isEmpty() ? new Polyfill( nativeVersion, polyfillVersion, polyfillName, polyfillName + "$install();") : new Polyfill(nativeVersion, polyfillVersion, "", ""); staticsBuilder.put(className, polyfill); staticsBuilder.put(GLOBAL + className, polyfill); staticsBuilder.put(WINDOW + className, polyfill); } return this; } /** Builds the {@link Polyfills}. */ Polyfills build() { return new Polyfills(this); } } } // TODO(sdh): ES6 output is still incomplete, so it's reasonable to use // --language_out=ES5 even if targetting ES6 browsers - we need to find a way // to distinguish this case and not give warnings for implemented features. private static final Polyfills POLYFILLS = new Polyfills.Builder() // Polyfills not (yet) implemented. .addClasses(ES6, ES6, "", "Proxy", "Reflect") .addClasses(ES6_IMPL, ES6_IMPL, "", "WeakMap", "WeakSet") // TODO(sdh): typed arrays??? these are implemented everywhere except in IE9, // and introducing warnings would be problematic. .addStatics(ES6_IMPL, ES6_IMPL, "", "Object", "getOwnPropertySymbols", "setPrototypeOf") .addStatics(ES6_IMPL, ES6_IMPL, "", "String", "raw") .addMethods(ES6_IMPL, ES6_IMPL, "", "normalize") // Implemented elsewhere (so no rewrite here) .addClasses(ES6_IMPL, ES3, "", "Symbol") // NOTE: The following polyfills will be implemented ASAP. Once each is implemented, // its output language will be changed from ES6 to ES3 and the polyfill namespace // ($jscomp or $jscomp.*) will replace the empty string argument indicating that the // polyfill should actually be used. // Implemented classes. .addClasses(ES6_IMPL, ES3, "$jscomp", "Map", "Set") // Math methods. .addStatics( ES6_IMPL, ES3, "$jscomp.math", "Math", "clz32", "imul", "sign", "log2", "log10", "log1p", "expm1", "cosh", "sinh", "tanh", "acosh", "asinh", "atanh", "hypot", "trunc", "cbrt") // Number methods. .addStatics( ES6_IMPL, ES3, "$jscomp.number", "Number", "isFinite", "isInteger", "isNaN", "isSafeInteger", "EPSILON", "MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER") // Object methods. .addStatics(ES6_IMPL, ES3, "$jscomp.object", "Object", "assign", "is") // (Soon-to-be implemented) String methods. .addStatics(ES6_IMPL, ES6_IMPL, "", "String", "fromCodePoint") .addMethods( ES6_IMPL, ES6_IMPL, "", "repeat", "codePointAt", "includes", "startsWith", "endsWith") // Array methods. .addStatics(ES6_IMPL, ES3, "$jscomp.array", "Array", "from", "of") .addMethods( ES6_IMPL, ES3, "$jscomp.array", "entries", "keys", "values", "copyWithin", "fill", "find", "findIndex") .build(); private final AbstractCompiler compiler; private final Polyfills polyfills; private GlobalNamespace globals; public RewritePolyfills(AbstractCompiler compiler) { this(compiler, POLYFILLS); } // Visible for testing RewritePolyfills(AbstractCompiler compiler, Polyfills polyfills) { this.compiler = compiler; this.polyfills = polyfills; } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { Traverser traverser = new Traverser(); NodeTraversal.traverseEs6(compiler, scriptRoot, traverser); if (traverser.changed) { compiler.needsEs6Runtime = true; compiler.reportCodeChange(); } } @Override public void process(Node externs, Node root) { if (languageOutIsAtLeast(ES6) || !compiler.getOptions().rewritePolyfills) { return; // no rewriting in this case. } this.globals = new GlobalNamespace(compiler, externs, root); hotSwapScript(root, null); } private static class InjectedInstaller { final JSModule module; final String installer; InjectedInstaller(JSModule module, String installer) { this.module = module; this.installer = installer; } @Override public int hashCode() { return Objects.hash(module, installer); } @Override public boolean equals(@Nullable Object other) { return other instanceof InjectedInstaller && ((InjectedInstaller) other).installer.equals(installer) && Objects.equals(((InjectedInstaller) other).module, module); } } private class Traverser extends AbstractPostOrderCallback { Set<InjectedInstaller> installers = new HashSet<>(); boolean changed = false; @Override public void visit(NodeTraversal traversal, Node node, Node parent) { // Fix types in JSDoc. JSDocInfo doc = node.getJSDocInfo(); if (doc != null) { fixJsdoc(traversal.getScope(), doc); } // Find qualified names that match static calls if (node.isQualifiedName()) { String name = node.getQualifiedName(); Polyfill polyfill = null; if (polyfills.statics.containsKey(name)) { polyfill = polyfills.statics.get(name); } if (polyfill != null) { // Check the scope to make sure it's a global name. if (isRootInScope(node, traversal) || NodeUtil.isVarOrSimpleAssignLhs(node, parent)) { return; } if (!languageOutIsAtLeast(polyfill.polyfillVersion)) { traversal.report( node, INSUFFICIENT_OUTPUT_VERSION_ERROR, name, compiler.getOptions().getLanguageOut().toString(), polyfill.polyfillVersion.toString()); } if (!languageOutIsAtLeast(polyfill.nativeVersion)) { if (!polyfill.installer.isEmpty()) { // Note: add the installer *before* replacing the node! addInstaller(node, polyfill.installer); } if (!polyfill.rewrite.isEmpty()) { changed = true; Node replacement = NodeUtil.newQName(compiler, polyfill.rewrite); replacement.useSourceInfoIfMissingFromForTree(node); parent.replaceChild(node, replacement); } } // TODO(sdh): consider warning if language_in is too low? it's not really any // harm, and we can't do it consistently for the prototype methods, so maybe // it's not worth doing here, either. return; // isGetProp (below) overlaps, so just bail out now } } // Add any requires that *might* match method calls (but don't rewrite anything) if (node.isGetProp() && node.getLastChild().isString()) { for (Polyfill polyfill : polyfills.methods.get(node.getLastChild().getString())) { if (!languageOutIsAtLeast(polyfill.nativeVersion) && !polyfill.installer.isEmpty()) { // Check if this is a global function. if (!isStaticFunction(node, traversal)) { addInstaller(node, polyfill.installer); } } } } } private boolean isStaticFunction(Node node, NodeTraversal traversal) { if (!node.isQualifiedName()) { return false; } String root = NodeUtil.getRootOfQualifiedName(node).getQualifiedName(); if (globals == null) { return false; } GlobalNamespace.Name fullName = globals.getOwnSlot(node.getQualifiedName()); GlobalNamespace.Name rootName = globals.getOwnSlot(root); if (fullName == null || rootName == null) { return false; } GlobalNamespace.Ref rootDecl = rootName.getDeclaration(); if (rootDecl == null) { return false; } Node globalDeclNode = rootDecl.getNode(); if (globalDeclNode == null) { return false; // don't know where the root came from so assume it could be anything } Var rootScope = traversal.getScope().getVar(root); if (rootScope == null) { return true; // root is not in the current scope, so it's a static function } Node scopeDeclNode = rootScope.getNode(); return scopeDeclNode == globalDeclNode; // is the global name currently in scope? } // Fix all polyfill type references in any JSDoc. private void fixJsdoc(Scope scope, JSDocInfo doc) { for (Node node : doc.getTypeNodes()) { fixJsdocType(scope, node); } } private void fixJsdocType(Scope scope, Node node) { if (node.isString()) { Polyfill polyfill = polyfills.statics.get(node.getString()); // Note: all classes are unqualified names, so we don't need to deal with dots if (polyfill != null && scope.getVar(node.getString()) == null && !languageOutIsAtLeast(polyfill.nativeVersion)) { node.setString(polyfill.rewrite); } } for (Node child = node.getFirstChild(); child != null; child = child.getNext()) { fixJsdocType(scope, child); } } private void addInstaller(Node sourceNode, String function) { // Find the module InputId inputId = sourceNode.getInputId(); CompilerInput input = inputId != null ? compiler.getInput(inputId) : null; JSModule module = input != null ? input.getModule() : null; InjectedInstaller injected = new InjectedInstaller(module, function); if (installers.add(injected)) { changed = true; Node installer = compiler.parseSyntheticCode(function).removeChildren(); installer.useSourceInfoIfMissingFromForTree(sourceNode); Node enclosingScript = NodeUtil.getEnclosingScript(sourceNode); enclosingScript.addChildrenToFront(installer); } } } private boolean languageOutIsAtLeast(LanguageMode mode) { return compiler.getOptions().getLanguageOut().compareTo(mode) >= 0; } private boolean languageOutIsAtLeast(FeatureSet features) { switch (features.version()) { case "ts": return languageOutIsAtLeast(LanguageMode.ECMASCRIPT6_TYPED); case "es6": case "es6-impl": // TODO(sdh): support a separate language mode for es6-impl? return languageOutIsAtLeast(LanguageMode.ECMASCRIPT6); case "es5": return languageOutIsAtLeast(LanguageMode.ECMASCRIPT5); case "es3": return languageOutIsAtLeast(LanguageMode.ECMASCRIPT3); default: return false; } } private static boolean isRootInScope(Node node, NodeTraversal traversal) { String rootName = NodeUtil.getRootOfQualifiedName(node).getQualifiedName(); return traversal.getScope().getVar(rootName) != null; } }