/*
* 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 static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.basetree.AbstractNodeVisitor;
import com.google.template.soy.basetree.CopyState;
import com.google.template.soy.basetree.Node;
import com.google.template.soy.basetree.NodeVisitor;
import com.google.template.soy.basetree.ParentNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.ParentExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.VarDefn;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.soytree.SoyNode.ExprHolderNode;
import com.google.template.soy.soytree.SoyNode.LocalVarNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* Shared utilities for the 'soytree' package.
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
*/
public final class SoyTreeUtils {
private static final Joiner COMMA_JOINER = Joiner.on(", ");
private SoyTreeUtils() {}
/** Returns true if the given {@code node} contains any children of the given types. */
@SafeVarargs
public static boolean hasNodesOfType(Node node, final Class<? extends Node>... types) {
class Visitor implements NodeVisitor<Node, Boolean> {
boolean found;
@Override
public Boolean exec(Node node) {
for (Class type : types) {
if (type.isInstance(node)) {
found = true;
return false; // short circuit
}
}
return true; // keep going
}
}
Visitor v = new Visitor();
visitAllNodes(node, v);
return v.found;
}
/**
* Runs the visitor on all nodes (including {@link ExprNode expr nodes}) reachable from the given
* node. The order of visiting is undefined.
*
* <p>If the visitor return {@code false} from {@link NodeVisitor#exec(Node)} we will short
* circuit visiting.
*/
public static void visitAllNodes(Node node, NodeVisitor<? super Node, Boolean> visitor) {
Deque<Node> queue = new ArrayDeque<>();
queue.add(node);
Node current;
while ((current = queue.pollLast()) != null) {
if (!visitor.exec(current)) {
return;
}
if (current instanceof ParentNode<?>) {
queue.addAll(((ParentNode<?>) current).getChildren());
}
if (current instanceof ExprHolderNode) {
for (ExprRootNode union : ((ExprHolderNode) current).getExprList()) {
queue.add(union);
}
}
}
}
/**
* Retrieves all nodes in a tree that are an instance of a particular class.
*
* @param <T> The type of node to retrieve.
* @param rootSoyNode The parse tree to search.
* @param classObject The class whose instances to search for, including subclasses.
* @return The nodes in the order they appear.
*/
public static <T extends Node> List<T> getAllNodesOfType(
SoyNode rootSoyNode, final Class<T> classObject) {
return getAllNodesOfType(rootSoyNode, classObject, true);
}
/**
* Retrieves all nodes in a tree that are an instance of a particular class.
*
* @param <T> The type of node to retrieve.
* @param rootSoyNode The parse tree to search.
* @param classObject The class whose instances to search for, including subclasses.
* @param doSearchSubtreesOfMatchedNodes Whether to keep searching subtrees of matched nodes for
* more nodes of the given type.
* @return The nodes in the order they appear.
*/
public static <T extends Node> List<T> getAllNodesOfType(
SoyNode rootSoyNode, final Class<T> classObject,
final boolean doSearchSubtreesOfMatchedNodes) {
final ImmutableList.Builder<T> matchedNodesBuilder = ImmutableList.builder();
final boolean exploreExpressions = ExprNode.class.isAssignableFrom(classObject);
final AbstractNodeVisitor<ExprNode, Void> exprVisitor =
exploreExpressions
? new AbstractNodeVisitor<ExprNode, Void>() {
@Override protected void visit(ExprNode exprNode) {
if (classObject.isInstance(exprNode)) {
matchedNodesBuilder.add(classObject.cast(exprNode));
if (!doSearchSubtreesOfMatchedNodes) {
return;
}
}
if (exprNode instanceof ParentExprNode) {
visitChildren((ParentExprNode) exprNode);
}
}
}
: null;
AbstractNodeVisitor<SoyNode, Void> visitor =
new AbstractNodeVisitor<SoyNode, Void>() {
@Override
protected void visit(SoyNode soyNode) {
if (classObject.isInstance(soyNode)) {
matchedNodesBuilder.add(classObject.cast(soyNode));
if (!doSearchSubtreesOfMatchedNodes) {
return;
}
}
if (soyNode instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) soyNode);
}
if (exploreExpressions && soyNode instanceof ExprHolderNode) {
for (ExprRootNode expr : ((ExprHolderNode) soyNode).getExprList()) {
exprVisitor.exec(expr);
}
}
}
};
visitor.exec(rootSoyNode);
return matchedNodesBuilder.build();
}
// -----------------------------------------------------------------------------------------------
// Utils for executing an ExprNode visitor on all expressions in a Soy tree.
/**
* Given a Soy node and a visitor for expression trees, traverses the subtree of the node and
* executes the visitor on all expressions held by nodes in the subtree.
*
* <p> Only processes expressions in V2 syntax. Ignores all expressions in V1 syntax.
*
* @param <R> The ExprNode visitor's return type.
* @param node The root of the subtree to visit all expressions in.
* @param exprNodeVisitor The visitor to execute on all expressions.
*/
public static <R> void execOnAllV2Exprs(
SoyNode node,
AbstractNodeVisitor<ExprNode, R> exprNodeVisitor) {
new VisitAllV2ExprsVisitor<R>(exprNodeVisitor).exec(node);
}
/**
* Private helper class for {@code visitAllExprs} and {@code visitAllExprsShortcircuitably}.
*
* @param <R> The ExprNode visitor's return type.
*/
private static final class VisitAllV2ExprsVisitor<R> extends AbstractNodeVisitor<SoyNode, R> {
private final AbstractNodeVisitor<ExprNode, R> exprNodeVisitor;
private VisitAllV2ExprsVisitor(AbstractNodeVisitor<ExprNode, R> exprNodeVisitor) {
this.exprNodeVisitor = exprNodeVisitor;
}
@Override protected void visit(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
if (node instanceof ExprHolderNode) {
for (ExprRootNode expr : ((ExprHolderNode) node).getExprList()) {
exprNodeVisitor.exec(expr);
}
}
}
}
// -----------------------------------------------------------------------------------------------
// Utils for cloning.
/**
* Clones the given node and then generates and sets new ids on all the cloned nodes (by default,
* SoyNode.copy(copyState) creates cloned nodes with the same ids as the original nodes).
*
* <p> This function will use the original Soy tree's node id generator to generate the new node
* ids for the cloned nodes. Thus, the original node to be cloned must be part of a full Soy tree.
* However, this does not mean that the cloned node will become part of the original tree (unless
* it is manually attached later). The cloned node will be an independent subtree with parent set
* to null.
*
* @param <T> The type of the node being cloned.
* @param origNode The original node to be cloned. This node must be part of a full Soy tree,
* because the generator for the new node ids will be retrieved from the root (SoyFileSetNode)
* of the tree.
* @param nodeIdGen The ID generator used for the tree.
* @return The cloned node, with all new ids for its subtree.
*/
public static <T extends SoyNode> T cloneWithNewIds(T origNode, IdGenerator nodeIdGen) {
// Clone the node.
T clone = cloneNode(origNode);
// Generate new ids.
(new GenNewIdsVisitor(nodeIdGen)).exec(clone);
return clone;
}
/**
* Clones the given list of nodes and then generates and sets new ids on all the cloned nodes (by
* default, SoyNode.copy(copyState) creates cloned nodes with the same ids as the original nodes).
*
* <p> This function will use the original Soy tree's node id generator to generate the new node
* ids for the cloned nodes. Thus, the original nodes to be cloned must be part of a full Soy
* tree. However, this does not mean that the cloned nodes will become part of the original tree
* (unless they are manually attached later). The cloned nodes will be independent subtrees with
* parents set to null.
*
* @param <T> The type of the nodes being cloned.
* @param origNodes The original nodes to be cloned. These nodes must be part of a full Soy tree,
* because the generator for the new node ids will be retrieved from the root (SoyFileSetNode)
* of the tree.
* @param nodeIdGen The ID generator used for the tree.
* @return The cloned nodes, with all new ids for their subtrees.
*/
public static <T extends SoyNode> List<T> cloneListWithNewIds(
List<T> origNodes, IdGenerator nodeIdGen) {
Preconditions.checkNotNull(origNodes);
List<T> clones = new ArrayList<>(origNodes.size());
for (T origNode : origNodes) {
T clone = cloneNode(origNode);
(new GenNewIdsVisitor(nodeIdGen)).exec(clone);
clones.add(clone);
}
return clones;
}
/**
* Clones a SoyNode but unlike SoyNode.copy(copyState) keeps {@link VarRefNode#getDefnDecl()}
* pointing at the correct tree.
*/
public static <T extends SoyNode> T cloneNode(T original) {
@SuppressWarnings("unchecked") // this holds for all SoyNode types
// TODO(lukes): eliminate this method once all logic has been moved into copy state
T cloned = (T) original.copy(new CopyState());
// TODO(lukes): this is not efficient but it is the only way to work around the limitations
// of the Object.copy(copyState) interface. A better solution would be to introduce our own
// clone method which could take a parameter. For nodes in the AST object graph that are the
// back edges in cycles (e.g. LocalVar) we could maintain an identity map which could be used to
// efficiently reconstruct the cycles. For now we just fix it up after the fact.
// All vardefns in varrefs have been invalidated. Currently we only reassign vardefns for
// LocalVarNodes because those have links (via declaringNode()) back up the tree, so we need to
// make sure that the declaringNode() links are correctly defined to point at the new tree
// instead of the previous one.
List<LocalVarNode> originalLocalVarNodes = getAllNodesOfType(original, LocalVarNode.class);
List<LocalVarNode> newLocalVarNodes = getAllNodesOfType(cloned, LocalVarNode.class);
Map<VarDefn, VarDefn> replacementMap = new IdentityHashMap<>();
for (int i = 0; i < newLocalVarNodes.size(); i++) {
VarDefn oldDefn = originalLocalVarNodes.get(i).getVar();
VarDefn newDefn = newLocalVarNodes.get(i).getVar();
checkState(oldDefn.name().equals(newDefn.name())); // sanity check
replacementMap.put(oldDefn, newDefn);
}
// limiting this to just local vars would make sense.
for (VarRefNode varRef : getAllNodesOfType(cloned, VarRefNode.class)) {
VarDefn replacement = replacementMap.get(varRef.getDefnDecl());
if (replacement != null) {
varRef.setDefn(replacement);
}
}
return cloned;
}
/**
* Private helper for cloneWithNewIds() to set new ids on a cloned subtree.
*/
private static class GenNewIdsVisitor extends AbstractNodeVisitor<SoyNode, Void> {
/** The generator for new node ids. */
private IdGenerator nodeIdGen;
/**
* @param nodeIdGen The generator for new node ids.
*/
public GenNewIdsVisitor(IdGenerator nodeIdGen) {
this.nodeIdGen = nodeIdGen;
}
@Override protected void visit(SoyNode node) {
node.setId(nodeIdGen.genId());
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
}
// -----------------------------------------------------------------------------------------------
// Miscellaneous.
/** Returns true if {@code node} is a descendant of {@code ancestor}. */
public static boolean isDescendantOf(SoyNode node, SoyNode ancestor) {
for (; node != null; node = node.getParent()) {
if (ancestor == node) {
return true;
}
}
return false;
}
public static String toSourceString(List<? extends Node> nodes) {
List<String> strings = new ArrayList<String>(nodes.size());
for (Node node : nodes) {
strings.add(node.toSourceString());
}
return COMMA_JOINER.join(strings);
}
}