/*
* Copyright 2013 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 static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.template.soy.passes.FindTransitiveDepTemplatesVisitor.TransitiveDepTemplatesInfo;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.CallBasicNode;
import com.google.template.soy.soytree.CallDelegateNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.TemplateBasicNode;
import com.google.template.soy.soytree.TemplateDelegateNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Visitor for finding the set of templates transitively called by a given template.
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p>{@link #exec} should be called on a {@code TemplateNode}.
*
* <p>If you need to call this visitor for multiple templates in the same tree (without modifying
* the tree), it's more efficient to reuse the same instance of this visitor because we memoize
* results from previous calls to exec.
*
*/
public final class FindTransitiveDepTemplatesVisitor
extends AbstractSoyNodeVisitor<TransitiveDepTemplatesInfo> {
/** Return value for {@code FindTransitiveDepTemplatesVisitor}. */
public static class TransitiveDepTemplatesInfo {
/** Set of templates transitively called by the root template(s). Sorted by template name. */
public final ImmutableSortedSet<TemplateNode> depTemplateSet;
/** @param depTemplateSet Set of templates transitively called by the root template(s). */
public TransitiveDepTemplatesInfo(Set<TemplateNode> depTemplateSet) {
this.depTemplateSet =
ImmutableSortedSet.copyOf(
new Comparator<TemplateNode>() {
@Override
public int compare(TemplateNode o1, TemplateNode o2) {
return o1.getTemplateName().compareTo(o2.getTemplateName());
}
},
depTemplateSet);
}
/**
* Merges multiple TransitiveDepTemplatesInfo objects (which may be TransitiveDepTemplatesInfo
* objects) into a single TransitiveDepTemplatesInfo, i.e. where the depTemplateSet is the union
* of the given info objects' depTemplateSets.
*/
public static TransitiveDepTemplatesInfo merge(
Iterable<? extends TransitiveDepTemplatesInfo> infosToMerge) {
ImmutableSet.Builder<TemplateNode> depTemplateSetBuilder = ImmutableSet.builder();
for (TransitiveDepTemplatesInfo infoToMerge : infosToMerge) {
depTemplateSetBuilder.addAll(infoToMerge.depTemplateSet);
}
return new TransitiveDepTemplatesInfo(depTemplateSetBuilder.build());
}
// Note: No need to override equals() and hashCode() here because we reuse instances of this
// class, which means the default behavior using object identity is sufficient.
}
// -----------------------------------------------------------------------------------------------
// Class for info collected about a specific template during a pass.
/**
* Class for info collected about a specific template during a pass.
*
* <p>We also refer to this as the unfinished info, as opposed to TransitiveDepTemplatesInfo,
* which is the finished info.
*/
private static class TemplateVisitInfo {
/** The root template that this info object is for. */
public final TemplateNode rootTemplate;
/** The template's position in the visit order of the templates visited during this pass. */
public final int visitOrdinal;
/**
* If nonnull, then this is a reference to the info object for the earliest known equivalent
* template, where "equivalent" means that either template can reach the other via calls (thus
* they should have the same finished TransitiveDepTemplatesInfo at the end), and "earliest" is
* by visit order of the templates visited during this pass.
*
* <p>Note: If nonnull, then the fields below (depTemplateSet, hasExternalCalls, hasDelCalls)
* may be incorrect even after the visit to the template has completed, because the correct info
* will be retrieved via this reference.
*/
public TemplateVisitInfo visitInfoOfEarliestEquivalent;
/**
* Set of templates transitively called by the root template.
*
* <p>Note: May be incomplete if visitInfoOfEarliestEquivalent is nonnull.
*/
public Set<TemplateNode> depTemplateSet;
/** Cached value of the finished info if previously computed, else null. */
private TransitiveDepTemplatesInfo finishedInfo;
public TemplateVisitInfo(TemplateNode template, int visitOrdinal) {
this.rootTemplate = template;
this.visitOrdinal = visitOrdinal;
this.visitInfoOfEarliestEquivalent = null;
this.depTemplateSet = Sets.newHashSet();
this.finishedInfo = null;
}
/**
* Updates the reference to the earliest known equivalent template's visit info, unless we
* already knew about the same or an even earlier equivalent.
*
* @param visitInfoOfNewEquivalent A newly discovered earlier equivalent template's visit info.
*/
public void maybeUpdateEarliestEquivalent(TemplateVisitInfo visitInfoOfNewEquivalent) {
Preconditions.checkArgument(visitInfoOfNewEquivalent != this);
if (this.visitInfoOfEarliestEquivalent == null
|| visitInfoOfNewEquivalent.visitOrdinal
< this.visitInfoOfEarliestEquivalent.visitOrdinal) {
this.visitInfoOfEarliestEquivalent = visitInfoOfNewEquivalent;
}
}
/**
* Incorporates finished info of a callee into this info object.
*
* @param calleeFinishedInfo The finished info to incorporate.
*/
public void incorporateCalleeFinishedInfo(TransitiveDepTemplatesInfo calleeFinishedInfo) {
depTemplateSet.addAll(calleeFinishedInfo.depTemplateSet);
}
/**
* Incorporates visit info of a callee into this info object.
*
* @param calleeVisitInfo The visit info to incorporate.
* @param activeTemplateSet The set of currently active templates (templates that we are in the
* midst of visiting, where the visit call has begun but has not ended).
*/
public void incorporateCalleeVisitInfo(
TemplateVisitInfo calleeVisitInfo, Set<TemplateNode> activeTemplateSet) {
if (calleeVisitInfo.visitInfoOfEarliestEquivalent == null
|| calleeVisitInfo.visitInfoOfEarliestEquivalent == this) {
// Cases 1 and 2: The callee doesn't have an earliest known equivalent (case 1), or it's the
// current template (case 2). We handle these together because in either case, we don't need
// to inherit the earliest known equivalent from the callee.
incorporateCalleeVisitInfoHelper(calleeVisitInfo);
} else if (activeTemplateSet.contains(
calleeVisitInfo.visitInfoOfEarliestEquivalent.rootTemplate)) {
// Case 3: The callee knows about some earlier equivalent (not this template) in the active
// visit path. Any earlier equivalent of the callee is also an equivalent of this template.
maybeUpdateEarliestEquivalent(calleeVisitInfo.visitInfoOfEarliestEquivalent);
incorporateCalleeVisitInfoHelper(calleeVisitInfo);
} else {
// Case 4: The callee's earliest known equivalent is not active (visit to that equivalent
// template has already ended). In this case, we instead want to incorporate that equivalent
// template's info (which should already have incorporated all of the callee's info, making
// the callee's own info unnecessary).
incorporateCalleeVisitInfo(
calleeVisitInfo.visitInfoOfEarliestEquivalent, activeTemplateSet);
}
}
/** Private helper for incorporateCalleeVisitInfo(). */
private void incorporateCalleeVisitInfoHelper(TemplateVisitInfo calleeVisitInfo) {
depTemplateSet.addAll(calleeVisitInfo.depTemplateSet);
}
/**
* Converts this (unfinished) visit info into a (finished) TransitiveDepTemplatesInfo.
*
* <p>Caches the result so that only one finished info object is created even if called multiple
* times.
*
* @return The finished info object.
*/
public TransitiveDepTemplatesInfo toFinishedInfo() {
if (finishedInfo == null) {
if (visitInfoOfEarliestEquivalent != null) {
finishedInfo = visitInfoOfEarliestEquivalent.toFinishedInfo();
} else {
finishedInfo = new TransitiveDepTemplatesInfo(depTemplateSet);
}
}
return finishedInfo;
}
}
// -----------------------------------------------------------------------------------------------
// FindTransitiveDepTemplatesVisitor body.
/** Registry of all templates in the Soy tree. */
private final TemplateRegistry templateRegistry;
/**
* Map from template node to finished info containing memoized info that was found in previous
* passes (previous calls to exec).
*/
@VisibleForTesting Map<TemplateNode, TransitiveDepTemplatesInfo> templateToFinishedInfoMap;
/** Visit info for the current template whose body we're visiting. */
private TemplateVisitInfo currTemplateVisitInfo;
/**
* Stack of active visit infos corresponding to the current visit/call path, i.e. for templates
* that we are in the midst of visiting, where the visit call has begun but has not ended
*/
private Deque<TemplateVisitInfo> activeTemplateVisitInfoStack;
/** Set of active templates, where "active" means the same thing as above. */
private Set<TemplateNode> activeTemplateSet;
/** Map from visited template (visit may or may not have ended) to visit info. */
private Map<TemplateNode, TemplateVisitInfo> visitedTemplateToInfoMap;
/** @param templateRegistry Map from template name to TemplateNode to use during the pass. */
public FindTransitiveDepTemplatesVisitor(TemplateRegistry templateRegistry) {
this.templateRegistry = checkNotNull(templateRegistry);
templateToFinishedInfoMap = Maps.newHashMap();
}
/**
* {@inheritDoc}
*
* <p>Note: This method is not thread-safe. If you need to get transitive dep templates info in a
* thread-safe manner, then please use {@link #execOnAllTemplates}() in a thread-safe manner.
*/
@Override
public TransitiveDepTemplatesInfo exec(SoyNode rootTemplate) {
Preconditions.checkArgument(rootTemplate instanceof TemplateNode);
TemplateNode rootTemplateCast = (TemplateNode) rootTemplate;
// If finished in a previous pass (previous call to exec), just return the finished info.
if (templateToFinishedInfoMap.containsKey(rootTemplateCast)) {
return templateToFinishedInfoMap.get(rootTemplateCast);
}
// Initialize vars for the pass.
currTemplateVisitInfo = null;
activeTemplateVisitInfoStack = new ArrayDeque<>();
activeTemplateSet = Sets.newHashSet();
visitedTemplateToInfoMap = Maps.newHashMap();
visit(rootTemplateCast);
if (!activeTemplateVisitInfoStack.isEmpty() || !activeTemplateSet.isEmpty()) {
throw new AssertionError();
}
// Convert visit info to finished info for all visited templates.
for (TemplateVisitInfo templateVisitInfo : visitedTemplateToInfoMap.values()) {
templateToFinishedInfoMap.put(
templateVisitInfo.rootTemplate, templateVisitInfo.toFinishedInfo());
}
return templateToFinishedInfoMap.get(rootTemplateCast);
}
/**
* Computes transitive dep templates info for multiple templates.
*
* <p>Note: This method returns a map from root template to TransitiveDepTemplatesInfo for the
* given root templates. If you wish to obtain a single TransitiveDepTemplatesInfo object that
* contains the combined info for all of the given root templates, then use
* TransitiveDepTemplatesInfo.merge(resultMap.values()) where resultMap is the result returned by
* this method.
*
* <p>Note: This method is not thread-safe. If you need to get transitive dep templates info in a
* thread-safe manner, then please use {@link #execOnAllTemplates}() in a thread-safe manner.
*
* @param rootTemplates The root templates to compute transitive dep templates info for.
* @return Map from root template to TransitiveDepTemplatesInfo.
*/
public ImmutableMap<TemplateNode, TransitiveDepTemplatesInfo> execOnMultipleTemplates(
Iterable<TemplateNode> rootTemplates) {
ImmutableMap.Builder<TemplateNode, TransitiveDepTemplatesInfo> resultBuilder =
ImmutableMap.builder();
for (TemplateNode rootTemplate : rootTemplates) {
resultBuilder.put(rootTemplate, exec(rootTemplate));
}
return resultBuilder.build();
}
/**
* Computes transitive dep templates info for all templates in a Soy tree.
*
* <p>Note: This method returns a map from root template to TransitiveDepTemplatesInfo for all
* templates in the given Soy tree. If you wish to obtain a single TransitiveDepTemplatesInfo
* object that contains the combined info for all templates in the given Soy tree, then use
* TransitiveDepTemplatesInfo.merge(resultMap.values()) where resultMap is the result returned by
* this method.
*
* <p>Note: This method is not thread-safe. If you need to get transitive dep templates info in a
* thread-safe manner, be sure to call this method only once and then use the precomputed map.
*
* @param soyTree A full Soy tree.
* @return Map from root template to TransitiveDepTemplatesInfo for all templates in the given Soy
* tree. The returned map is deeply immutable (TransitiveDepTemplatesInfo is immutable).
*/
public ImmutableMap<TemplateNode, TransitiveDepTemplatesInfo> execOnAllTemplates(
SoyFileSetNode soyTree) {
List<TemplateNode> allTemplates =
SoyTreeUtils.getAllNodesOfType(soyTree, TemplateNode.class, false);
return execOnMultipleTemplates(allTemplates);
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override
protected void visitTemplateNode(TemplateNode node) {
if (templateToFinishedInfoMap.containsKey(node)) {
throw new AssertionError(); // already visited in some previous pass (previous call to exec)
}
if (visitedTemplateToInfoMap.containsKey(node)) {
throw new AssertionError(); // already visited during the current pass
}
currTemplateVisitInfo = new TemplateVisitInfo(node, visitedTemplateToInfoMap.size());
visitedTemplateToInfoMap.put(node, currTemplateVisitInfo);
// Add this template.
currTemplateVisitInfo.depTemplateSet.add(node);
// Visit the template body to find calls to recurse on.
visitChildren(node);
currTemplateVisitInfo = null;
}
@Override
protected void visitCallBasicNode(CallBasicNode node) {
// Don't forget to visit content within CallParamContentNodes.
visitChildren(node);
TemplateBasicNode callee = templateRegistry.getBasicTemplate(node.getCalleeName());
// If the callee is null (i.e. not within the Soy file set), then this is an external call.
if (callee == null) {
return;
}
// Visit the callee template.
processCalleeHelper(callee);
}
@Override
protected void visitCallDelegateNode(CallDelegateNode node) {
// Don't forget to visit content within CallParamContentNodes.
visitChildren(node);
// Visit all the possible callee templates.
ImmutableList<TemplateDelegateNode> potentialCallees =
templateRegistry
.getDelTemplateSelector()
.delTemplateNameToValues()
.get(node.getDelCalleeName());
for (TemplateDelegateNode delCallee : potentialCallees) {
processCalleeHelper(delCallee);
}
}
/** Private helper for visitCallBasicNode() and visitCallDelegateNode(). */
private void processCalleeHelper(TemplateNode callee) {
if (templateToFinishedInfoMap.containsKey(callee)) {
// Case 1: The callee was already finished in a previous pass (previous call to exec).
currTemplateVisitInfo.incorporateCalleeFinishedInfo(templateToFinishedInfoMap.get(callee));
} else if (callee == currTemplateVisitInfo.rootTemplate) {
// Case 2: The callee is the current template (direct recursive call). Nothing to do here.
} else if (activeTemplateSet.contains(callee)) {
// Case 3: The callee is an ancestor in our depth-first visit tree. The callee (i.e.
// ancestor) is "equivalent" to the current template because either template can reach the
// the other via calls. In this case, we may change the field visitInfoOfEarliestEquivalent
// (unless we had previously already found an earlier equivalent).
currTemplateVisitInfo.maybeUpdateEarliestEquivalent(visitedTemplateToInfoMap.get(callee));
} else if (visitedTemplateToInfoMap.containsKey(callee)) {
// Case 4: The callee was visited sometime earlier in the current pass, and that visit has
// already ended since the callee is not in the activeTemplateSet (case 3 above).
currTemplateVisitInfo.incorporateCalleeVisitInfo(
visitedTemplateToInfoMap.get(callee), activeTemplateSet);
} else {
// Case 5: The callee is a new template we've never visited.
activeTemplateVisitInfoStack.push(currTemplateVisitInfo);
activeTemplateSet.add(currTemplateVisitInfo.rootTemplate);
visit(callee);
currTemplateVisitInfo = activeTemplateVisitInfoStack.pop();
activeTemplateSet.remove(currTemplateVisitInfo.rootTemplate);
currTemplateVisitInfo.incorporateCalleeVisitInfo(
visitedTemplateToInfoMap.get(callee), activeTemplateSet);
}
}
// -----------------------------------------------------------------------------------------------
// Fallback implementation.
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
}