/*
* Copyright 2017 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.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.basicfunctions.FloatFunction;
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.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.passes.FindIndirectParamsVisitor.IndirectParamsInfo;
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.CallParamContentNode;
import com.google.template.soy.soytree.CallParamNode;
import com.google.template.soy.soytree.CallParamValueNode;
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.TemplateDelegateNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import com.google.template.soy.soytree.defn.HeaderParam;
import com.google.template.soy.soytree.defn.TemplateParam;
import com.google.template.soy.soytree.defn.TemplateParam.DeclLoc;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyType.Kind;
import com.google.template.soy.types.SoyTypes;
import com.google.template.soy.types.aggregate.UnionType;
import com.google.template.soy.types.primitive.FloatType;
import com.google.template.soy.types.primitive.SanitizedType;
import com.google.template.soy.types.primitive.StringType;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
/**
* This compiler pass runs several checks on {@code CallNode}s.
*
* <ul>
* <li> Checks that calling arguments match the declared parameter types of the called template.
* <li> Checks missing or duplicate parameters.
* <li> Checks strict-html templates can only call other strict-html templates from an html
* context.
* </ul>
*
* <p>In addition to checking that static types match and flagging errors, this visitor also stores
* a set of TemplateParam object in each {@code CallNode} for all the params that require runtime
* checking.
*
* <p>Note: This pass requires that the ResolveExpressionTypesVisitor has already been run.
*/
final class CheckTemplateCallsPass extends CompilerFileSetPass {
static final SoyErrorKind ARGUMENT_TYPE_MISMATCH =
SoyErrorKind.of("Type mismatch on param {0}: expected: {1}, actual: {2}.");
private static final SoyErrorKind DUPLICATE_PARAM = SoyErrorKind.of("Duplicate param ''{0}''.");
private static final SoyErrorKind MISSING_PARAM = SoyErrorKind.of("Call missing required {0}.");
private static final SoyErrorKind PASSING_PROTOBUF_FROM_STRICT_TO_NON_STRICT =
SoyErrorKind.of("Passing protobuf {0} of type {1} to an untyped template is not allowed.");
private static final SoyErrorKind STRICT_HTML =
SoyErrorKind.of(
"Found call to non stricthtml template. Strict HTML template "
+ "can only call other strict HTML templates from an HTML context.");
/** Whether strict html mode is enabled for the compiler. */
private final boolean enabledStrictHtml;
/** The error reporter that is used in this compiler pass. */
private final ErrorReporter errorReporter;
CheckTemplateCallsPass(boolean enabledStrictHtml, ErrorReporter errorReporter) {
this.enabledStrictHtml = enabledStrictHtml;
this.errorReporter = errorReporter;
}
@Override
public void run(SoyFileSetNode fileSet, TemplateRegistry registry) {
new CheckCallsVisitor(registry).exec(fileSet);
}
private final class CheckCallsVisitor extends AbstractSoyNodeVisitor<Void> {
/** Registry of all templates in the Soy tree. */
private final TemplateRegistry templateRegistry;
/** The current template being checked. */
private TemplateNode callerTemplate;
/** Map of all template parameters, both explicit and implicit, organized by template. */
private final Map<TemplateNode, TemplateParamTypes> paramTypesMap = new HashMap<>();
CheckCallsVisitor(TemplateRegistry registry) {
this.templateRegistry = registry;
}
@Override
protected void visitCallBasicNode(CallBasicNode node) {
TemplateNode callee = templateRegistry.getBasicTemplate(node.getCalleeName());
if (callee != null) {
Set<TemplateParam> paramsToRuntimeCheck = checkCallParamTypes(node, callee);
node.setParamsToRuntimeCheck(paramsToRuntimeCheck);
checkCallParamNames(node, callee);
}
checkStrictHtml(node, callee);
visitChildren(node);
}
@Override
protected void visitCallDelegateNode(CallDelegateNode node) {
ImmutableMap.Builder<TemplateDelegateNode, ImmutableList<TemplateParam>>
paramsToCheckByTemplate = ImmutableMap.builder();
ImmutableList<TemplateDelegateNode> potentialCallees =
templateRegistry
.getDelTemplateSelector()
.delTemplateNameToValues()
.get(node.getDelCalleeName());
for (TemplateDelegateNode delTemplate : potentialCallees) {
Set<TemplateParam> params = checkCallParamTypes(node, delTemplate);
paramsToCheckByTemplate.put(delTemplate, ImmutableList.copyOf(params));
checkCallParamNames(node, delTemplate);
}
node.setParamsToRuntimeCheck(paramsToCheckByTemplate.build());
if (!potentialCallees.isEmpty()) {
checkStrictHtml(node, potentialCallees.get(0));
}
visitChildren(node);
}
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
@Override
protected void visitTemplateNode(TemplateNode node) {
callerTemplate = node;
visitChildren(node);
callerTemplate = null;
}
/**
* Returns the subset of {@link TemplateNode#getParams() callee params} that require runtime
* type checking.
*/
private Set<TemplateParam> checkCallParamTypes(CallNode call, TemplateNode callee) {
TemplateParamTypes calleeParamTypes = getTemplateParamTypes(callee);
// Explicit params being passed by the CallNode
Set<String> explicitParams = new HashSet<>();
// The set of params that need runtime type checking at template call time. We start this with
// all the params of the callee and remove each param that is statically verified.
Set<String> paramNamesToRuntimeCheck = new HashSet<>(calleeParamTypes.params.keySet());
// indirect params should be checked at their callsites, not this one.
paramNamesToRuntimeCheck.removeAll(calleeParamTypes.indirectParamNames);
// First check all the {param} blocks of the caller to make sure that the types match.
for (CallParamNode callerParam : call.getChildren()) {
String paramName = callerParam.getKey();
// The types of the parameters. If this is an explicitly declared parameter,
// then this collection will have only one member; If it's an implicit
// parameter then this may contain multiple types. Note we don't use
// a union here because the rules are a bit different than the normal rules
// for assigning union types.
// It's possible that the set may be empty, because all of the callees
// are external. In that case there's nothing we can do, so we don't
// report anything.
Collection<SoyType> declaredParamTypes = calleeParamTypes.params.get(paramName);
// Type of the param value. May be null if the param is a v1 expression.
SoyType argType = null;
if (callerParam.getKind() == SoyNode.Kind.CALL_PARAM_VALUE_NODE) {
CallParamValueNode node = (CallParamValueNode) callerParam;
ExprNode expr = node.getExpr();
if (expr != null) {
argType = maybeCoerceType(node, expr.getType(), declaredParamTypes);
}
} else if (callerParam.getKind() == SoyNode.Kind.CALL_PARAM_CONTENT_NODE) {
ContentKind contentKind = ((CallParamContentNode) callerParam).getContentKind();
argType =
contentKind == null
? StringType.getInstance()
: SanitizedType.getTypeForContentKind(contentKind);
} else {
throw new AssertionError(); // CallParamNode shouldn't have any other kind of child
}
// If the param is a v1 expression (so argType == null) we can't check anything, or if the
// calculated type is an error type (because we already reported a type error for this
// expression) don't check whether it matches the formal param.
if (argType != null && argType.getKind() != SoyType.Kind.ERROR) {
boolean staticTypeSafe = true;
for (SoyType formalType : declaredParamTypes) {
staticTypeSafe &=
checkArgumentAgainstParamType(
callerParam.getSourceLocation(),
paramName,
argType,
formalType,
calleeParamTypes);
}
if (staticTypeSafe) {
paramNamesToRuntimeCheck.remove(paramName);
}
}
explicitParams.add(paramName);
}
// If the caller is passing data via data="all" then we look for matching static param
// declarations in the callers template and see if there are type errors there.
if (call.dataAttribute().isPassingData()) {
if (call.dataAttribute().isPassingAllData() && callerTemplate.getParams() != null) {
// Check indirect params that are passed via data="all".
// We only need to check explicit params of calling template here.
for (TemplateParam callerParam : callerTemplate.getParams()) {
if (!(callerParam instanceof HeaderParam)) {
continue;
}
String paramName = callerParam.name();
// The parameter is explicitly overridden with another value, which we
// already checked.
if (explicitParams.contains(paramName)) {
continue;
}
Collection<SoyType> declaredParamTypes = calleeParamTypes.params.get(paramName);
boolean staticTypeSafe = true;
for (SoyType formalType : declaredParamTypes) {
staticTypeSafe &=
checkArgumentAgainstParamType(
call.getSourceLocation(),
paramName,
callerParam.type(),
formalType,
calleeParamTypes);
}
if (staticTypeSafe) {
paramNamesToRuntimeCheck.remove(paramName);
}
}
} else {
// TODO: Check if the fields of the type of data arg can be assigned to the params.
// This is possible for some types, and never allowed for other types.
}
}
/**
* We track the set as names above and transform to TemplateParams here because the above
* loops are over the {param}s of the caller and TemplateParams of the callers template, so
* all we have are the names of the parameters. To convert them to a TemplateParam of the
* callee we need to match the names and it is easier to do that as one pass at the end
* instead of iteratively throughout.
*/
Set<TemplateParam> paramsToRuntimeCheck = new HashSet<>();
for (TemplateParam param : callee.getParams()) {
if (paramNamesToRuntimeCheck.remove(param.name())) {
paramsToRuntimeCheck.add(param);
}
}
// sanity check
Preconditions.checkState(
paramNamesToRuntimeCheck.isEmpty(),
"Unexpected callee params %s",
paramNamesToRuntimeCheck);
return paramsToRuntimeCheck;
}
/**
* For int values passed into template param float, perform automatic type coercion from the
* call param value to the template param type.
*
* <p>Supported coercions:
*
* <ul>
* <li>int -> float
* <li>int -> float|null
* </ul>
*
* @param paramNode Node containing the call param value
* @param argType Type of the call param value
* @param declaredTypes Types accepted by the template param
* @return the new coerced type
*/
@CheckReturnValue
private SoyType maybeCoerceType(
CallParamValueNode paramNode, SoyType argType, Collection<SoyType> declaredTypes) {
if (argType.getKind() == Kind.INT
&& (declaredTypes.contains(FloatType.getInstance())
|| declaredTypes.contains(SoyTypes.makeNullable(FloatType.getInstance())))) {
for (SoyType formalType : declaredTypes) {
if (formalType.isAssignableFrom(argType)) {
return argType; // template already accepts int, no need to coerce
}
}
ExprRootNode root = paramNode.getExpr();
// create a node to wrap param in float coercion
FunctionNode newParam =
new FunctionNode(FloatFunction.INSTANCE.getName(), root.getRoot().getSourceLocation());
newParam.setSoyFunction(FloatFunction.INSTANCE);
newParam.setType(FloatType.getInstance());
newParam.addChild(root.getRoot());
root.addChild(newParam);
return FloatType.getInstance();
}
return argType;
}
/**
* Check that the argument passed to the template is compatible with the template parameter
* type.
*
* @param location The location to report a type check error.
* @param paramName the name of the parameter.
* @param argType The type of the value being passed.
* @param formalType The type of the parameter.
* @param calleeParams metadata about the callee parameters
* @return true if runtime type checks can be elided for this param
*/
private boolean checkArgumentAgainstParamType(
SourceLocation location,
String paramName,
SoyType argType,
SoyType formalType,
TemplateParamTypes calleeParams) {
if ((!calleeParams.isStrictlyTyped && formalType.getKind() == SoyType.Kind.UNKNOWN)
|| formalType.getKind() == SoyType.Kind.ANY) {
// Special rules for unknown / any
if (argType.getKind() == SoyType.Kind.PROTO) {
errorReporter.report(
location, PASSING_PROTOBUF_FROM_STRICT_TO_NON_STRICT, paramName, argType);
}
} else if (argType.getKind() == SoyType.Kind.UNKNOWN) {
// Special rules for unknown / any
//
// This check disabled: We now allow maps created from protos to be passed
// to a function accepting a proto, this makes migration easier.
// (See GenJsCodeVisitor.genParamTypeChecks).
// TODO(user): Re-enable at some future date?
// if (formalType.getKind() == SoyType.Kind.PROTO || SoyType.Kind.PROTO_ENUM) {
// reportProtoArgumentTypeMismatch(call, paramName, formalType, argType);
// }
} else {
if (!formalType.isAssignableFrom(argType)) {
if (calleeParams.isIndirect(paramName)
&& argType.getKind() == SoyType.Kind.UNION
&& ((UnionType) argType).isNullable()
&& SoyTypes.makeNullable(formalType).isAssignableFrom(argType)) {
// Special case for indirect params: Allow a nullable type to be assigned
// to a non-nullable type if the non-nullable type is an indirect parameter type.
// The reason is because without flow analysis, we can't know whether or not
// there are if-statements preventing null from being passed as an indirect
// param, so we assume all indirect params are optional.
return false;
}
errorReporter.report(location, ARGUMENT_TYPE_MISMATCH, paramName, formalType, argType);
}
}
return true;
}
/**
* Get the parameter types for a callee.
*
* @param node The template being called.
* @return The set of template parameters, both explicit and implicit.
*/
private TemplateParamTypes getTemplateParamTypes(TemplateNode node) {
TemplateParamTypes paramTypes = paramTypesMap.get(node);
if (paramTypes == null) {
paramTypes = new TemplateParamTypes();
// Store all of the explicitly declared param types
if (node.getParams() != null) {
for (TemplateParam param : node.getParams()) {
if (param.declLoc() == DeclLoc.SOY_DOC) {
paramTypes.isStrictlyTyped = false;
}
Preconditions.checkNotNull(param.type());
paramTypes.params.put(param.name(), param.type());
}
}
// Store indirect params where there's no conflict with explicit params.
// Note that we don't check here whether the explicit type and the implicit
// types are in agreement - that will be done when it's this template's
// turn to be analyzed as a caller.
IndirectParamsInfo ipi = new FindIndirectParamsVisitor(templateRegistry).exec(node);
for (String indirectParamName : ipi.indirectParamTypes.keySet()) {
if (paramTypes.params.containsKey(indirectParamName)) {
continue;
}
paramTypes.params.putAll(
indirectParamName, ipi.indirectParamTypes.get(indirectParamName));
paramTypes.indirectParamNames.add(indirectParamName);
}
// Save the param types map
paramTypesMap.put(node, paramTypes);
}
return paramTypes;
}
/**
* Helper method that reports an error if a strict html template calls a non-strict html
* template from HTML context.
*/
private void checkStrictHtml(CallNode caller, @Nullable TemplateNode callee) {
// We should only check strict html if 1) experimental feature is on; 2) the current template
// sets stricthtml to true, and 3) the current call node is in HTML context.
// Then we report an error if the callee is HTML but is not a strict HTML template.
if (enabledStrictHtml
&& callerTemplate.isStrictHtml()
&& caller.getIsPcData()
&& callee != null
&& callee.getContentKind() == ContentKind.HTML
&& !callee.isStrictHtml()) {
errorReporter.report(caller.getSourceLocation(), STRICT_HTML);
}
}
/**
* Helper method that reports an error if:
*
* <ul>
* <li> There are duplicate params for the caller.
* <li> Required parameters in callee template are not presented in the caller.
* </ul>
*/
private void checkCallParamNames(CallNode caller, TemplateNode callee) {
// If all the data keys being passed are listed using 'param' commands, then check that all
// required params of the callee are included.
if (!caller.dataAttribute().isPassingData()) {
// Do the check if the callee node has declared params.
if (callee != null && callee.getParams() != null) {
// Get param keys passed by caller.
Set<String> callerParamKeys = Sets.newHashSet();
for (CallParamNode callerParam : caller.getChildren()) {
boolean isUnique = callerParamKeys.add(callerParam.getKey());
if (!isUnique) {
// Found a duplicate param.
errorReporter.report(
callerParam.getSourceLocation(), DUPLICATE_PARAM, callerParam.getKey());
}
}
// Check param keys required by callee.
List<String> missingParamKeys = Lists.newArrayListWithCapacity(2);
for (TemplateParam calleeParam : callee.getParams()) {
if (calleeParam.isRequired() && !callerParamKeys.contains(calleeParam.name())) {
missingParamKeys.add(calleeParam.name());
}
}
// Report errors.
if (!missingParamKeys.isEmpty()) {
String errorMsgEnd =
(missingParamKeys.size() == 1)
? "param '" + missingParamKeys.get(0) + "'"
: "params " + missingParamKeys;
errorReporter.report(caller.getSourceLocation(), MISSING_PARAM, errorMsgEnd);
}
}
}
}
private class TemplateParamTypes {
public boolean isStrictlyTyped = true;
public final Multimap<String, SoyType> params = HashMultimap.create();
public final Set<String> indirectParamNames = new HashSet<>();
public boolean isIndirect(String paramName) {
return indirectParamNames.contains(paramName);
}
}
}
}