/*
* Copyright 2011 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 com.google.common.base.Equivalence;
import com.google.common.base.Preconditions;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.shared.internal.DelTemplateSelector;
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.TemplateBasicNode;
import com.google.template.soy.soytree.TemplateDelegateNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import com.google.template.soy.soytree.defn.TemplateParam;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* Checks various rules regarding the use of delegates (including delegate packages, delegate
* templates, and delegate calls).
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p>{@link #exec} should be called on a full parse tree. There is no return value. A {@code
* SoySyntaxException} is thrown if an error is found.
*
*/
final class CheckDelegatesVisitor extends AbstractSoyNodeVisitor<Void> {
private static final SoyErrorKind CALL_TO_DELTEMPLATE =
SoyErrorKind.of("''call'' to delegate template ''{0}'' (expected ''delcall'').");
private static final SoyErrorKind CROSS_PACKAGE_DELCALL =
SoyErrorKind.of(
"Found illegal call from ''{0}'' to ''{1}'', which is in a different delegate package.");
private static final SoyErrorKind DELCALL_TO_BASIC_TEMPLATE =
SoyErrorKind.of("''delcall'' to basic template ''{0}'' (expected ''call'').");
private static final SoyErrorKind DELTEMPLATES_WITH_DIFFERENT_PARAM_DECLARATIONS =
SoyErrorKind.of(
"Found delegate template with same name ''{0}'' but different param declarations "
+ "compared to the definition at {1}.");
private static final SoyErrorKind STRICT_DELTEMPLATES_WITH_DIFFERENT_CONTENT_KIND =
SoyErrorKind.of(
"If one deltemplate has strict autoescaping, all its peers must also be strictly"
+ " autoescaped with the same content kind: {0} != {1}. Conflicting definition at"
+ " {2}.");
private static final SoyErrorKind DELTEMPLATES_WITH_DIFFERENT_STRICT_HTML_MODE =
SoyErrorKind.of(
"Found delegate template with same name ''{0}'' but different strict html mode "
+ "compared to the definition at {1}.");
/** A template registry built from the Soy tree. */
private final TemplateRegistry templateRegistry;
/** The current enclosing template's name, as suitable for user messages (during pass). */
private String currTemplateNameForUserMsgs;
/** Current delegate package name, or null if none (during pass). */
private String currDelPackageName;
private final boolean enabledStrictHtml;
private final ErrorReporter errorReporter;
CheckDelegatesVisitor(
TemplateRegistry templateRegistry, boolean enabledStrictHtml, ErrorReporter errorReporter) {
this.templateRegistry = templateRegistry;
this.enabledStrictHtml = enabledStrictHtml;
this.errorReporter = errorReporter;
}
@Override
public Void exec(SoyNode soyNode) {
Preconditions.checkArgument(soyNode instanceof SoyFileSetNode);
// Perform checks that only involve templates (uses templateRegistry only, no traversal).
checkTemplates();
// Perform checks that involve calls (uses traversal).
super.exec(soyNode);
return null;
}
/** Performs checks that only involve templates (uses templateRegistry only). */
private void checkTemplates() {
DelTemplateSelector<TemplateDelegateNode> selector = templateRegistry.getDelTemplateSelector();
// Check that all delegate templates with the same name have the same declared params,
// content kind, and strict html mode.
for (Collection<TemplateDelegateNode> delTemplateGroup :
selector.delTemplateNameToValues().asMap().values()) {
TemplateDelegateNode firstDelTemplate = null;
Set<Equivalence.Wrapper<TemplateParam>> firstRequiredParamSet = null;
ContentKind firstContentKind = null;
boolean firstStrictHtml = false;
// loop over all members of the deltemplate group.
for (TemplateDelegateNode delTemplate : delTemplateGroup) {
if (firstDelTemplate == null) {
// First template encountered.
firstDelTemplate = delTemplate;
firstRequiredParamSet = getRequiredParamSet(delTemplate);
firstContentKind = delTemplate.getContentKind();
firstStrictHtml = delTemplate.isStrictHtml() && firstContentKind == ContentKind.HTML;
} else {
// Not first template encountered.
Set<Equivalence.Wrapper<TemplateParam>> currRequiredParamSet =
getRequiredParamSet(delTemplate);
if (!currRequiredParamSet.equals(firstRequiredParamSet)) {
errorReporter.report(
delTemplate.getSourceLocation(),
DELTEMPLATES_WITH_DIFFERENT_PARAM_DECLARATIONS,
firstDelTemplate.getDelTemplateName(),
firstDelTemplate.getSourceLocation().toString());
}
if (delTemplate.getContentKind() != firstContentKind) {
// TODO: This is only *truly* a requirement if the strict mode deltemplates are
// being called by contextual templates. For a strict-to-strict call, everything
// is escaped at runtime at the call sites. You could imagine delegating between
// either a plain-text or rich-html template. However, most developers will write
// their deltemplates in a parallel manner, and will want to know when the
// templates differ. Plus, requiring them all to be the same early-on will allow
// future optimizations to avoid the run-time checks, so it's better to start out
// as strict as possible and only open up if needed.
errorReporter.report(
delTemplate.getSourceLocation(),
STRICT_DELTEMPLATES_WITH_DIFFERENT_CONTENT_KIND,
String.valueOf(firstContentKind),
String.valueOf(delTemplate.getContentKind()),
firstDelTemplate.getSourceLocation().toString());
}
// Check if all del templates have the same settings of strict HTML mode.
// We do not need to check {@code ContentKind} again since we already did that earlier
// in this pass.
if (enabledStrictHtml && delTemplate.isStrictHtml() != firstStrictHtml) {
errorReporter.report(
delTemplate.getSourceLocation(),
DELTEMPLATES_WITH_DIFFERENT_STRICT_HTML_MODE,
firstDelTemplate.getDelTemplateName(),
firstDelTemplate.getSourceLocation().toString());
}
}
}
}
}
// A specific equivalence relation for seeing if the params of 2 difference templates are
// effectively the same.
private static final class ParamEquivalence extends Equivalence<TemplateParam> {
static final ParamEquivalence INSTANCE = new ParamEquivalence();
@Override
protected boolean doEquivalent(TemplateParam a, TemplateParam b) {
return a.name().equals(b.name())
&& a.isRequired() == b.isRequired()
&& a.isInjected() == b.isInjected()
&& a.type().equals(b.type());
}
@Override
protected int doHash(TemplateParam t) {
return Objects.hash(t.name(), t.isInjected(), t.isRequired(), t.type());
}
}
private static Set<Equivalence.Wrapper<TemplateParam>> getRequiredParamSet(
TemplateDelegateNode delTemplate) {
Set<Equivalence.Wrapper<TemplateParam>> paramSet = new HashSet<>();
for (TemplateParam param : delTemplate.getParams()) {
if (param.isRequired()) {
paramSet.add(ParamEquivalence.INSTANCE.wrap(param));
}
}
return paramSet;
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override
protected void visitTemplateNode(TemplateNode node) {
this.currTemplateNameForUserMsgs = node.getTemplateNameForUserMsgs();
this.currDelPackageName = node.getDelPackageName();
visitChildren(node);
}
@Override
protected void visitCallBasicNode(CallBasicNode node) {
String calleeName = node.getCalleeName();
// Check that the callee name is not a delegate template name.
if (templateRegistry.getDelTemplateSelector().hasDelTemplateNamed(calleeName)) {
errorReporter.report(node.getSourceLocation(), CALL_TO_DELTEMPLATE, calleeName);
}
// Check that the callee is either not in a delegate package or in the same delegate package.
TemplateBasicNode callee = templateRegistry.getBasicTemplate(calleeName);
if (callee != null) {
String calleeDelPackageName = callee.getDelPackageName();
if (calleeDelPackageName != null && !calleeDelPackageName.equals(currDelPackageName)) {
errorReporter.report(
node.getSourceLocation(),
CROSS_PACKAGE_DELCALL,
currTemplateNameForUserMsgs,
callee.getTemplateName());
}
}
}
@Override
protected void visitCallDelegateNode(CallDelegateNode node) {
String delCalleeName = node.getDelCalleeName();
// Check that the callee name is not a basic template name.
if (templateRegistry.getBasicTemplate(delCalleeName) != null) {
errorReporter.report(node.getSourceLocation(), DELCALL_TO_BASIC_TEMPLATE, delCalleeName);
}
}
// -----------------------------------------------------------------------------------------------
// Fallback implementation.
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
}