/* * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.template.soy.passes; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.SoyErrorKind; import com.google.template.soy.exprparse.SoyParsingContext; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.MsgPluralNode; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; /** * Visitor for finding {@code print} nodes that are actually {@code remainder} nodes, and replacing * them with the appropriate expression. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * * <p>{@link #exec} should be called on a full parse tree. There is no return value. * */ public final class RewriteRemaindersVisitor extends AbstractSoyNodeVisitor<Void> { private static final SoyErrorKind REMAINDER_ARITY_MISMATCH = SoyErrorKind.of("''remainder'' called with {0} arguments, expected 1."); private static final SoyErrorKind REMAINDER_OUTSIDE_PLURAL = SoyErrorKind.of("Special function ''remainder'' is for use in plural messages only."); private static final SoyErrorKind REMAINDER_PLURAL_EXPR_MISMATCH = SoyErrorKind.of("Argument to ''remainder'' must be the same as the ''plural'' variable"); private static final SoyErrorKind REMAINDER_UNNECESSARY_AT_OFFSET_0 = SoyErrorKind.of("''remainder'' is unnecessary since offset=0."); private static final SoyErrorKind REMAINDER_WITH_PHNAME = SoyErrorKind.of("Special function ''remainder'' cannot be used with ''phname''."); /** The MsgPluralNode most recently visited. */ private MsgPluralNode currPluralNode; private final ErrorReporter errorReporter; public RewriteRemaindersVisitor(ErrorReporter errorReporter) { this.errorReporter = errorReporter; } // ----------------------------------------------------------------------------------------------- // Implementations for specific nodes. @Override protected void visitPrintNode(PrintNode node) { // We cannot easily access the original context. However, because everything has already been // parsed, that should be fine. I don't think this can fail at all, but whatever. SoyParsingContext context = SoyParsingContext.empty(errorReporter, "fake.namespace"); ExprRootNode exprRootNode = node.getExpr(); // Check for the function node with the function "remainder()". if (exprRootNode.getRoot() instanceof FunctionNode) { FunctionNode functionNode = (FunctionNode) exprRootNode.getRoot(); if (functionNode.getFunctionName().equals("remainder")) { // 'remainder' outside 'plural'. Bad! if (currPluralNode == null) { errorReporter.report(functionNode.getSourceLocation(), REMAINDER_OUTSIDE_PLURAL); return; // to prevent NPE later in the method } // 'remainder' with no parameters or more than one parameter. Bad! if (functionNode.numChildren() != 1) { errorReporter.report( functionNode.getSourceLocation(), REMAINDER_ARITY_MISMATCH, functionNode.numChildren()); } // 'remainder' with a different expression than the enclosing 'plural'. Bad! if (!functionNode .getChild(0) .toSourceString() .equals(currPluralNode.getExpr().toSourceString())) { errorReporter.report(functionNode.getSourceLocation(), REMAINDER_PLURAL_EXPR_MISMATCH); } // 'remainder' with a 0 offset. Bad! if (currPluralNode.getOffset() == 0) { errorReporter.report(functionNode.getSourceLocation(), REMAINDER_UNNECESSARY_AT_OFFSET_0); } // 'remainder' with 'phname' attribute. Bad! if (node.getUserSuppliedPhName() != null) { errorReporter.report(functionNode.getSourceLocation(), REMAINDER_WITH_PHNAME); } // Now rewrite the PrintNode (reusing the old node id). String newExprText = "(" + currPluralNode.getExpr().toSourceString() + ") - " + currPluralNode.getOffset(); PrintNode newPrintNode = new PrintNode.Builder(node.getId(), node.isImplicit(), SourceLocation.UNKNOWN) .exprText(newExprText) .build(context); newPrintNode.addChildren(node.getChildren()); node.getParent().replaceChild(node, newPrintNode); } } } @Override protected void visitMsgPluralNode(MsgPluralNode node) { currPluralNode = node; visitChildren(node); currPluralNode = null; } // ----------------------------------------------------------------------------------------------- // Fallback implementation. @Override protected void visitSoyNode(SoyNode node) { if (node instanceof ParentSoyNode<?>) { visitChildrenAllowingConcurrentModification((ParentSoyNode<?>) node); } } }