/*
* 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.Sets;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.exprparse.SoyParsingContext;
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.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.RawTextNode;
import com.google.template.soy.soytree.SoyFileNode;
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.SoyNode.StandaloneNode;
import com.google.template.soy.soytree.TemplateNode;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Applies changes specified in {@link Inferences} to a Soy parse tree.
*
*/
final class Rewriter {
/** The changes to make. */
private final Inferences inferences;
/**
* The names of templates visited. Used to distinguish derived templates from templates in the
* input Soy files.
*/
private final Set<String> visitedTemplateNames = Sets.newHashSet();
/** Maps print directive names to the content kinds they consume and produce. */
private final Map<String, ContentKind> sanitizedContentOperators;
/** For reporting errors. */
private final ErrorReporter errorReporter;
Rewriter(
Inferences inferences,
Map<String, ContentKind> sanitizedContentOperators,
ErrorReporter errorReporter) {
this.inferences = inferences;
this.sanitizedContentOperators = sanitizedContentOperators;
this.errorReporter = errorReporter;
}
/** @return Derived templates that should be added to the parse tree. */
public List<TemplateNode> rewrite(SoyFileSetNode files) {
RewriterVisitor mutator = new RewriterVisitor();
// First walk the input files that the caller already knows about.
for (SoyFileNode file : files.getChildren()) {
mutator.exec(file);
}
// Now walk over anything not reachable from the input files to make sure we get all the derived
// templates.
ImmutableList.Builder<TemplateNode> extraTemplates = ImmutableList.builder();
for (TemplateNode template : inferences.getAllTemplates()) {
String name = template.getTemplateName();
if (!visitedTemplateNames.contains(name)) {
extraTemplates.add(template);
mutator.exec(template);
}
}
return extraTemplates.build();
}
/** A visitor that applies the changes in Inferences to a Soy tree. */
private final class RewriterVisitor extends AbstractSoyNodeVisitor<Void> {
/** Keep track of template nodes so we know which are derived and which aren't. */
@Override
protected void visitTemplateNode(TemplateNode templateNode) {
Preconditions.checkState(!visitedTemplateNames.contains(templateNode.getTemplateName()));
visitedTemplateNames.add(templateNode.getTemplateName());
visitChildrenAllowingConcurrentModification(templateNode);
}
/** Add any escaping directives. */
@Override
protected void visitPrintNode(PrintNode printNode) {
ImmutableList<EscapingMode> escapingModes = inferences.getEscapingModesForNode(printNode);
for (EscapingMode escapingMode : escapingModes) {
PrintDirectiveNode newPrintDirective =
new PrintDirectiveNode.Builder(
inferences.getIdGenerator().genId(),
escapingMode.directiveName,
"",
printNode.getSourceLocation())
.build(SoyParsingContext.exploding());
// Figure out where to put the new directive.
// Normally they go at the end to ensure that the value printed is of the appropriate type,
// but if there are SanitizedContentOperators at the end, then make sure that their input
// is of the appropriate type since we know that they will not change the content type.
int newPrintDirectiveIndex = printNode.numChildren();
while (newPrintDirectiveIndex > 0) {
String printDirectiveName = printNode.getChild(newPrintDirectiveIndex - 1).getName();
ContentKind contentKind = sanitizedContentOperators.get(printDirectiveName);
if (contentKind == null || contentKind != escapingMode.contentKind) {
break;
}
--newPrintDirectiveIndex;
}
printNode.addChild(newPrintDirectiveIndex, newPrintDirective);
}
}
/** Do nothing. */
@Override
protected void visitRawTextNode(RawTextNode rawTextNode) {
// TODO: Possibly normalize raw text nodes by adding quotes around unquoted attributes with
// non-noescape dynamic content to avoid the need for space escaping.
}
/** Grabs the inferred escaping directives from the node in string form. */
private ImmutableList<String> getDirectiveNamesForNode(SoyNode node) {
ImmutableList.Builder<String> escapingDirectiveNames = new ImmutableList.Builder<>();
for (EscapingMode escapingMode : inferences.getEscapingModesForNode(node)) {
escapingDirectiveNames.add(escapingMode.directiveName);
}
return escapingDirectiveNames.build();
}
/** Sets the escaping directives we inferred on the node. */
@Override
protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
node.setEscapingDirectiveNames(getDirectiveNamesForNode(node));
visitChildren(node);
}
/**
* Rewrite call targets.
*
* <p>Note that this processing is only applicable for CallBasicNodes. The reason is that
* CallDelegateNodes are always calling public templates (delegate templates are always public),
* and public templates never need rewriting.
*
* <p>TODO: Modify contextual autoescape to deal with delegates appropriately.
*/
@Override
protected void visitCallNode(CallNode callNode) {
// 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");
String derivedCalleeName = inferences.getDerivedCalleeNameForCall(callNode);
if (derivedCalleeName != null) {
// Creates a new call node, but with a different target name.
// TODO: Create a CallNode.withNewName() convenience method.
CallNode newCallNode;
if (callNode instanceof CallBasicNode) {
// For simplicity, use the full callee name as the source callee name.
newCallNode =
new CallBasicNode.Builder(callNode.getId(), callNode.getSourceLocation())
.calleeName(derivedCalleeName)
.sourceCalleeName(derivedCalleeName)
.dataAttribute(callNode.dataAttribute())
.userSuppliedPlaceholderName(callNode.getUserSuppliedPhName())
.syntaxVersionBound(callNode.getSyntaxVersionUpperBound())
.escapingDirectiveNames(callNode.getEscapingDirectiveNames())
.build(context);
} else {
CallDelegateNode callNodeCast = (CallDelegateNode) callNode;
newCallNode =
new CallDelegateNode.Builder(callNode.getId(), callNode.getSourceLocation())
.delCalleeName(derivedCalleeName)
.delCalleeVariantExpr(callNodeCast.getDelCalleeVariantExpr())
.allowEmptyDefault(callNodeCast.allowsEmptyDefault())
.dataAttribute(callNode.dataAttribute())
.userSuppliedPlaceholderName(callNode.getUserSuppliedPhName())
.escapingDirectiveNames(callNode.getEscapingDirectiveNames())
.build(context);
}
if (!callNode.getCommandText().equals(newCallNode.getCommandText())) {
moveChildrenTo(callNode, newCallNode);
replaceChild(callNode, newCallNode);
}
// Ensure we visit the new node instead of the old one.
callNode = newCallNode;
}
// For strict templates, set any necessary escaping directives.
callNode.setEscapingDirectiveNames(getDirectiveNamesForNode(callNode));
visitChildrenAllowingConcurrentModification(callNode);
}
/** Recurses to children. */
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildrenAllowingConcurrentModification((ParentSoyNode<?>) node);
}
}
}
/** Replaces old child with new child. */
private static void replaceChild(StandaloneNode oldChild, StandaloneNode newChild) {
oldChild.getParent().replaceChild(oldChild, newChild);
}
private static <T extends SoyNode> void moveChildrenTo(
ParentSoyNode<T> oldParent, ParentSoyNode<T> newParent) {
List<T> children = ImmutableList.copyOf(oldParent.getChildren());
oldParent.clearChildren();
newParent.addChildren(children);
}
}