/*
* Copyright 2010 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.parsepasses.contextautoesc;
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.Maps;
import com.google.common.collect.Sets;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.shared.restricted.SoyPrintDirective;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.EscapingMode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.PrintDirectiveNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.TemplateBasicNode;
import com.google.template.soy.soytree.TemplateBasicNodeBuilder;
import com.google.template.soy.soytree.TemplateDelegateNode;
import com.google.template.soy.soytree.TemplateDelegateNodeBuilder;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateNode.SoyFileHeaderInfo;
import com.google.template.soy.soytree.defn.TemplateParam;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Encapsulates information inferred about a Soy file and decisions made to change it.
*
* <p>
* The mutator methods on this class do not change the Soy file. All changes are delayed until
* after all inference has been done so that we can safely try a variety of speculative techniques.
*
* <p>
* To make it easier to do speculative inference, this class cascades : each instance has a parent,
* and it delegates to that when it does not have info itself.
* And there is a {@link Inferences#foldIntoParent()} method that propagates all decisions into the
* parent when a set of inference decisions are considered final.
*
* <p>
* The {@link ContextualAutoescaper} creates a single root instance and its passes fold successful
* inferences into the parent until it ends up with a final set of rewriting decisions that the
* {@link Rewriter} applies to the input Soy parse tree.
*
*/
final class Inferences {
/** Null or an instance to inherit state from. */
private final @Nullable Inferences parent;
/**
* Soy directives that cancel autoescaping (see {@link SoyPrintDirective#shouldCancelAutoescape}).
*/
private final ImmutableSet<String> autoescapeCancellingDirectives;
/** Used to generate unique IDs for cloned templates. */
private final IdGenerator idGen;
/** Map of template names to instances used to type <code>{call}</code> commands. */
private final Map<String, List<TemplateNode>> templatesByName = Maps.newLinkedHashMap();
/** The types of templates. */
private final Map<String, Context> templateNameToEndContext = Maps.newLinkedHashMap();
/** Maps IDs of <code>{print}</code> commands to the context in which they start. */
private final Map<SoyNode, Context> nodeToStartContext = Maps.newIdentityHashMap();
/** Maps IDs of print and call commands to the inferred escaping modes. */
private final Map<SoyNode, ImmutableList<EscapingMode>> nodeToEscapingModes =
Maps.newIdentityHashMap();
/** Maps IDs of <code>{call}</code> commands to the derived template they should use. */
private final Map<CallNode, String> callNodeToDerivedCalleeName = Maps.newIdentityHashMap();
/** The set of template names checked. Used to identify re-entrant templates. */
private final Set<String> templatesChecked = Sets.newHashSet();
/**
* An instance that inherits from a parent.
*/
public Inferences(Inferences parent) {
this.parent = parent;
this.autoescapeCancellingDirectives = parent.autoescapeCancellingDirectives;
this.idGen = parent.idGen;
}
/**
* An instance that does not inherit from a parent.
*
* @param autoescapeCancellingDirectives Soy directives that
* {@link SoyPrintDirective#shouldCancelAutoescape cancel} autoescaping.
* @param idGen Used to generate unique IDs for cloned templates.
* @param templatesByName Map of template names to instances used to type <code>{call}</code>
* commands.
*/
public Inferences(
Set<String> autoescapeCancellingDirectives, IdGenerator idGen,
Map<String, ImmutableList<TemplateNode>> templatesByName) {
this.parent = null;
this.autoescapeCancellingDirectives = ImmutableSet.copyOf(autoescapeCancellingDirectives);
this.idGen = idGen;
this.templatesByName.putAll(templatesByName);
}
/**
* Stores a type conclusion. This may be speculative.
* @param templateName A qualified template name.
*/
public void recordTemplateEndContext(String templateName, Context context) {
templateNameToEndContext.put(templateName, context);
}
/**
* Finds the named templates.
* @param templateName A qualified template name.
*/
public List<TemplateNode> lookupTemplates(String templateName) {
for (Inferences inferences = this; inferences != null; inferences = inferences.parent) {
List<TemplateNode> tn = inferences.templatesByName.get(templateName);
if (tn != null) {
return tn;
}
}
return null;
}
/**
* Null if no typing has been done for the named template, or otherwise the context after a call
* to the named template. Since we derive templates by start context at the call site, there
* is no start context parameter.
*
* @param templateName A qualified template name.
*/
public Context getTemplateEndContext(String templateName) {
for (Inferences inferences = this; inferences != null; inferences = inferences.parent) {
Context oc = inferences.templateNameToEndContext.get(templateName);
if (oc != null) {
return oc;
}
}
return null;
}
/**
* Null if there is no escaping mode for the given <code>{print}</code> node.
*/
public ImmutableList<EscapingMode> getEscapingMode(PrintNode printNode) {
// See if we have already inferred an escaping mode for the node.
for (Inferences inferences = this; inferences != null; inferences = inferences.parent) {
ImmutableList<EscapingMode> escapingModes = inferences.nodeToEscapingModes.get(printNode);
if (escapingModes != null) {
return escapingModes;
}
}
// Look for an escaping mode in the existing directives.
ImmutableList.Builder<EscapingMode> modes = ImmutableList.builder();
for (PrintDirectiveNode directive : printNode.getChildren()) {
String directiveName = directive.getName();
EscapingMode dirMode = EscapingMode.fromDirective(directiveName);
if (dirMode != null) {
modes.add(dirMode);
} else if (autoescapeCancellingDirectives.contains(directiveName)) {
modes.add(EscapingMode.NO_AUTOESCAPE);
}
}
return modes.build();
}
/**
* Records inferred escaping modes so a directive can be later added to the Soy parse tree.
*/
public void setEscapingDirectives(
SoyNode node, Context startContext, List<EscapingMode> escapingModes) {
Preconditions.checkArgument(
(node instanceof PrintNode)
|| (node instanceof CallNode)
|| (node instanceof MsgFallbackGroupNode),
"Escaping directives may only be set for {print}, {msg}, or {call} nodes");
nodeToStartContext.put(node, startContext);
if (escapingModes != null) {
nodeToEscapingModes.put(node, ImmutableList.copyOf(escapingModes));
}
}
/**
* The escaping modes for the print command with the given ID in the order in which they should be
* applied.
*
* @param node a node instance
*/
public ImmutableList<EscapingMode> getEscapingModesForNode(SoyNode node) {
ImmutableList<EscapingMode> modes = nodeToEscapingModes.get(node);
if (modes == null) {
modes = ImmutableList.of();
}
return modes;
}
public ImmutableMap<SoyNode, Context> getPrintNodeStartContexts() {
return ImmutableMap.copyOf(nodeToStartContext);
}
/**
* Derives a <code>{call}</code> site so that it uses a version of the template appropriate to
* the start context.
* @param derivedCalleeName A qualified template name.
*/
public void retargetCall(CallNode cn, String derivedCalleeName) {
callNodeToDerivedCalleeName.put(cn, derivedCalleeName);
}
/**
* The name of the derived template that the call with the given id should call, or null if the
* call with the given id should not be retargeted to a derived template.
*/
public String getDerivedCalleeNameForCall(CallNode callNode) {
return callNodeToDerivedCalleeName.get(callNode);
}
/**
* Clones a template, changing the name.
* @return A copy of tn, differing semantically only in name and auto-generated IDs.
* The new templates will be available via {@link #lookupTemplates} with the given name.
*/
public List<TemplateNode> cloneTemplates(String baseName, String derivedName) {
if (lookupTemplates(derivedName) != null) {
throw new AssertionError(derivedName);
}
ImmutableList.Builder<TemplateNode> b = ImmutableList.builder();
for (TemplateNode tn : lookupTemplates(baseName)) {
SoyFileHeaderInfo soyFileHeaderInfo = tn.getSoyFileHeaderInfo();
// We trivially clone the template with new ids, this ensures that all the varrefs have proper
// vardefns assigned. Then we manually recopy to a new template in order to modify the name.
// TODO(lukes): we should add direct support for swapping template names to TemplateNode
// or just eliminate usecases for this method.
TemplateNode trivialClonedTemplate = SoyTreeUtils.cloneWithNewIds(tn, idGen);
int cloneId = trivialClonedTemplate.getId();
// We need to use the unnamespaced name in the command text since we'll be inserting this
// template into a file node that already has a namespace declaration.
TemplateNode clone;
if (tn instanceof TemplateBasicNode) {
String derivedPartialName = (tn.getPartialTemplateName() != null) ?
derivedName.substring(soyFileHeaderInfo.namespace.length()) : null;
clone =
new TemplateBasicNodeBuilder(soyFileHeaderInfo, ExplodingErrorReporter.get())
.setId(cloneId)
.setSourceLocation(tn.getSourceLocation())
.setCmdTextInfo(
derivedName,
derivedPartialName,
tn.getVisibility(),
tn.getAutoescapeMode(),
tn.getContentKind(),
tn.getRequiredCssNamespaces())
.addParams(trivialClonedTemplate.getAllParams())
.build();
if (! (derivedName.equals(clone.getTemplateName()) &&
Objects.equals(derivedPartialName, clone.getPartialTemplateName()))) {
throw new AssertionError();
}
} else if (tn instanceof TemplateDelegateNode) {
TemplateDelegateNode tdn = (TemplateDelegateNode) tn;
clone =
new TemplateDelegateNodeBuilder(soyFileHeaderInfo, ExplodingErrorReporter.get())
.setId(cloneId)
.setSourceLocation(tn.getSourceLocation())
.setCmdTextInfo(
derivedName,
tdn.getDelTemplateVariant(),
tdn.getDelPriority(),
tn.getAutoescapeMode(),
tn.getContentKind(),
tn.getRequiredCssNamespaces())
.addParams(trivialClonedTemplate.getAllParams())
.build();
if (! (derivedName.equals(((TemplateDelegateNode) clone).getDelTemplateName()))) {
throw new AssertionError();
}
} else {
throw new AssertionError("Unknown template node type: " + tn.getClass());
}
clone.addChildren(trivialClonedTemplate.getChildren());
// Reassign all the local variable data which isn't maintained by the cloning process above.
clone.setMaxLocalVariableTableSize(tn.getMaxLocalVariableTableSize());
Iterator<TemplateParam> tnIterator = tn.getAllParams().iterator();
Iterator<TemplateParam> cloneIterator = clone.getAllParams().iterator();
while (tnIterator.hasNext()) {
cloneIterator.next().setLocalVariableIndex(tnIterator.next().localVariableIndex());
}
b.add(clone);
}
ImmutableList<TemplateNode> clones = b.build();
templatesByName.put(derivedName, clones);
return clones;
}
/**
* Folds speculative decisions into the parent passed to the constructor.
* This instance should not be used after folding.
*/
public void foldIntoParent() {
parent.nodeToEscapingModes.putAll(nodeToEscapingModes);
parent.nodeToStartContext.putAll(nodeToStartContext);
parent.templateNameToEndContext.putAll(templateNameToEndContext);
parent.callNodeToDerivedCalleeName.putAll(callNodeToDerivedCalleeName);
parent.templatesByName.putAll(templatesByName);
parent.templatesChecked.addAll(templatesChecked);
}
/**
* All known templates.
*/
public List<TemplateNode> getAllTemplates() {
ImmutableList.Builder<TemplateNode> b = ImmutableList.builder();
for (List<TemplateNode> templates : templatesByName.values()) {
b.addAll(templates);
}
return b.build();
}
/**
* Indicates that a template was visited.
* @see #wasTemplateChecked
*/
public void recordTemplateChecked(String templateName) {
templatesChecked.add(templateName);
}
/**
* True if {@link #recordTemplateChecked} was called with the same template name.
*/
public boolean wasTemplateChecked(String templateName) {
return templatesChecked.contains(templateName);
}
/**
* The id generator used for newly created nodes.
*/
public IdGenerator getIdGenerator() {
return idGen;
}
}