/* * Copyright 2008 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.soytree; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.basetree.CopyState; import com.google.template.soy.error.SoyErrorKind; import com.google.template.soy.exprparse.ExpressionParser; import com.google.template.soy.exprparse.SoyParsingContext; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.exprtree.IntegerNode; import com.google.template.soy.exprtree.VarRefNode; import com.google.template.soy.soytree.SoyNode.ConditionalBlockNode; import com.google.template.soy.soytree.SoyNode.ExprHolderNode; import com.google.template.soy.soytree.SoyNode.LocalVarBlockNode; import com.google.template.soy.soytree.SoyNode.LoopNode; import com.google.template.soy.soytree.SoyNode.StandaloneNode; import com.google.template.soy.soytree.SoyNode.StatementNode; import com.google.template.soy.soytree.defn.LocalVar; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Node representing a 'for' statement. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * */ public final class ForNode extends AbstractBlockCommandNode implements StandaloneNode, StatementNode, ConditionalBlockNode, LoopNode, ExprHolderNode, LocalVarBlockNode { /** The arguments to a {@code range(...)} expression in a {@code {for ...}} loop statement. */ @AutoValue public abstract static class RangeArgs { private static final RangeArgs ERROR = create(VarRefNode.ERROR, VarRefNode.ERROR, VarRefNode.ERROR); static RangeArgs create(ExprNode start, ExprNode limit, ExprNode increment) { return new AutoValue_ForNode_RangeArgs( new ExprRootNode(start), new ExprRootNode(limit), new ExprRootNode(increment)); } RangeArgs() {} /** The expression for the iteration start point. Default is {@code 0}. */ public abstract ExprRootNode start(); /** The expression for the iteration end point. This is interpreted as an exclusive limit. */ public abstract ExprRootNode limit(); /** The expression for the iteration increment. Default is {@code 1}. */ public abstract ExprRootNode increment(); /** * Returns true if it is statically known that the range is not empty. * * <p>Currently this is only possible if we have a range that contains constant values. */ public final boolean definitelyNotEmpty() { long start; if (start().getRoot() instanceof IntegerNode) { start = ((IntegerNode) start().getRoot()).getValue(); } else { return false; // if the start is not a constant then it might be empty } long limit; if (limit().getRoot() instanceof IntegerNode) { limit = ((IntegerNode) limit().getRoot()).getValue(); } else { return false; } // NOTE: we don't need to consider the increment, since as long as start < limit, the start // will always be produced by the range return start < limit; } private RangeArgs copy(CopyState copyState) { return create( start().getRoot().copy(copyState), limit().getRoot().copy(copyState), increment().getRoot().copy(copyState)); } } private static final SoyErrorKind INVALID_COMMAND_TEXT = SoyErrorKind.of("Invalid ''for'' command text"); private static final SoyErrorKind INVALID_RANGE_SPECIFICATION = SoyErrorKind.of("Invalid range specification"); private static final SoyErrorKind RANGE_OUT_OF_RANGE = SoyErrorKind.of("Range specification is too large: {0}"); /** Regex pattern for the command text. */ // 2 capturing groups: local var name, arguments to range() private static final Pattern COMMAND_TEXT_PATTERN = Pattern.compile( "( [$] \\w+ ) \\s+ in \\s+ range[(] \\s* (.*) \\s* [)]", Pattern.COMMENTS | Pattern.DOTALL); /** The Local variable for this loop. */ private final LocalVar var; /** The parsed range args. */ private final RangeArgs rangeArgs; /** * @param id The id for this node. * @param commandText The command text. * @param sourceLocation The source location for the {@code for }node. */ public ForNode( int id, String commandText, SourceLocation sourceLocation, SoyParsingContext context) { super(id, sourceLocation, "for", commandText); Matcher matcher = COMMAND_TEXT_PATTERN.matcher(commandText); if (!matcher.matches()) { context.report(sourceLocation, INVALID_COMMAND_TEXT); this.rangeArgs = RangeArgs.ERROR; this.var = new LocalVar("error", this, null); // Return early to avoid IllegalStateException below return; } String varName = parseVarName(matcher.group(1), sourceLocation, context); List<ExprNode> rangeArgs = parseRangeArgs(matcher.group(2), sourceLocation, context); if (rangeArgs.size() > 3 || rangeArgs.isEmpty()) { context.report(sourceLocation, INVALID_RANGE_SPECIFICATION); this.rangeArgs = RangeArgs.ERROR; } else { // OK, now interpret the args // If there are 2 or more args, then the first is the 'start' value; default is 0 ExprNode start = rangeArgs.size() >= 2 ? rangeArgs.get(0) : new IntegerNode(0, sourceLocation); // If there are 3 args, then the last one is the increment; default is 1 ExprNode increment = rangeArgs.size() == 3 ? rangeArgs.get(2) : new IntegerNode(1, sourceLocation); // the limit is the first item if there is only one arg, otherwise it is the second arg ExprNode limit = rangeArgs.get(rangeArgs.size() == 1 ? 0 : 1); this.rangeArgs = RangeArgs.create(start, limit, increment); // Range args cannot be larger than 32-bit ints if (isOutOfRange(start)) { context.report(sourceLocation, RANGE_OUT_OF_RANGE, ((IntegerNode) start).getValue()); } if (isOutOfRange(increment)) { context.report(sourceLocation, RANGE_OUT_OF_RANGE, ((IntegerNode) increment).getValue()); } if (isOutOfRange(limit)) { context.report(sourceLocation, RANGE_OUT_OF_RANGE, ((IntegerNode) limit).getValue()); } } var = new LocalVar(varName, this, null); } private static String parseVarName( String input, SourceLocation sourceLocation, SoyParsingContext context) { return new ExpressionParser(input, sourceLocation, context).parseVariable().getName(); } private static List<ExprNode> parseRangeArgs( String input, SourceLocation sourceLocation, SoyParsingContext context) { return new ExpressionParser(input, sourceLocation, context).parseExpressionList(); } private static boolean isOutOfRange(ExprNode node) { if (node instanceof IntegerNode) { long n = ((IntegerNode) node).getValue(); return n > Integer.MAX_VALUE || n < Integer.MIN_VALUE; } return false; } /** * Copy constructor. * * @param orig The node to copy. */ private ForNode(ForNode orig, CopyState copyState) { super(orig, copyState); this.var = new LocalVar(orig.var, this); this.rangeArgs = orig.rangeArgs.copy(copyState); } @Override public Kind getKind() { return Kind.FOR_NODE; } @Override public final LocalVar getVar() { return var; } @Override public final String getVarName() { return var.name(); } /** Returns the parsed range args. */ public RangeArgs getRangeArgs() { return rangeArgs; } @Override public ImmutableList<ExprRootNode> getExprList() { return ImmutableList.of(rangeArgs.start(), rangeArgs.limit(), rangeArgs.increment()); } @SuppressWarnings("unchecked") @Override public ParentSoyNode<StandaloneNode> getParent() { return (ParentSoyNode<StandaloneNode>) super.getParent(); } @Override public ForNode copy(CopyState copyState) { return new ForNode(this, copyState); } }