/*
* 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.Joiner;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.basetree.SyntaxVersion;
import com.google.template.soy.basicfunctions.LengthFunction;
import com.google.template.soy.basicfunctions.ParseFloatFunction;
import com.google.template.soy.basicfunctions.ParseIntFunction;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.ErrorReporter.Checkpoint;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprtree.AbstractExprNodeVisitor;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.ParentExprNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.MapLiteralNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.defn.LoopVar;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.primitive.AnyType;
import com.google.template.soy.types.primitive.StringType;
import java.util.Set;
/**
* Checks the signatures of functions.
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
*/
final class CheckFunctionCallsPass extends CompilerFilePass {
private static final SoyErrorKind INCORRECT_NUM_ARGS =
SoyErrorKind.of("Function ''{0}'' called with {1} arguments (expected {2}).");
private static final SoyErrorKind INCORRECT_ARG_TYPE =
SoyErrorKind.of("Function ''{0}'' called with incorrect arg type {1} (expected {2}).");
private static final SoyErrorKind LOOP_VARIABLE_NOT_IN_SCOPE =
SoyErrorKind.of("Function ''{0}'' must have a foreach loop variable as its argument.");
private static final SoyErrorKind QUOTE_KEYS_IF_JS_REQUIRES_MAP_LITERAL_ARG =
SoyErrorKind.of(
"Function ''quoteKeysIfJs'' called with argument of type {0} (expected map literal).");
private static final SoyErrorKind V1_EXPRESSION_REQUIRES_STRING_ARG =
SoyErrorKind.of(
"Function ''v1Expression'' called with argument of type {0} (expected string literal).");
private static final SoyErrorKind UNKNOWN_FUNCTION = SoyErrorKind.of("Unknown function ''{0}''.");
private final ErrorReporter errorReporter;
private final boolean allowUnknownFunction;
private final CheckFunctionCallsExprVisitor exprNodeVisitor = new CheckFunctionCallsExprVisitor();
/** User-declared syntax version. */
private SyntaxVersion declaredSyntaxVersion;
CheckFunctionCallsPass(
boolean allowUnknownFunctions,
SyntaxVersion declaredSyntaxVersion,
ErrorReporter errorReporter) {
this.allowUnknownFunction = allowUnknownFunctions;
this.errorReporter = errorReporter;
this.declaredSyntaxVersion = declaredSyntaxVersion;
}
@Override
public void run(SoyFileNode file, IdGenerator nodeIdGen) {
SoyTreeUtils.execOnAllV2Exprs(file, exprNodeVisitor);
}
/**
* Used to visit expr nodes to find nonplugin and basic functions and check their signatures and
* arg types.
*/
private final class CheckFunctionCallsExprVisitor extends AbstractExprNodeVisitor<Void> {
/** Recurse to children. */
@Override
protected void visitExprNode(ExprNode node) {
if (node instanceof ParentExprNode) {
visitChildren((ParentExprNode) node);
}
}
/** Check the function signature. */
@Override
protected void visitFunctionNode(FunctionNode node) {
String fnName = node.getFunctionName();
SoyFunction function = node.getSoyFunction();
if (function == null) {
if (declaredSyntaxVersion != SyntaxVersion.V1_0 && !allowUnknownFunction) {
// In Soy V2, all functions must be available as SoyFunctions at compile time.
errorReporter.report(node.getSourceLocation(), UNKNOWN_FUNCTION, fnName);
}
return;
}
Checkpoint checkpoint = errorReporter.checkpoint();
checkNumArgs(function, node);
// If there were arity errors, don't run further function visits
if (!errorReporter.errorsSince(checkpoint)) {
if (function instanceof BuiltinFunction) {
visitNonpluginFunction((BuiltinFunction) function, node);
} else {
visitFunction(function, node);
}
}
// Recurse to operands.
visitChildren(node);
}
private void visitNonpluginFunction(BuiltinFunction nonpluginFn, FunctionNode node) {
// All non-plugin functions so far have exactly 1 arg
ExprNode arg = node.getChild(0);
// Check argument types.
switch (nonpluginFn) {
case INDEX:
case IS_FIRST:
case IS_LAST:
requireLoopVariableInScope(node, arg);
break;
case QUOTE_KEYS_IF_JS:
if (!(arg instanceof MapLiteralNode)) {
errorReporter.report(
node.getSourceLocation(),
QUOTE_KEYS_IF_JS_REQUIRES_MAP_LITERAL_ARG,
arg.getType().toString());
}
break;
case CHECK_NOT_NULL:
// Do nothing. All types are valid.
break;
case V1_EXPRESSION:
if (!(arg instanceof StringNode)) {
errorReporter.report(
node.getSourceLocation(),
V1_EXPRESSION_REQUIRES_STRING_ARG,
node.getChild(0).getType().toString());
}
break;
default:
throw new AssertionError("Unrecognized nonplugin fn " + nonpluginFn.getName());
}
}
private void visitFunction(SoyFunction fn, FunctionNode node) {
// TODO(user): This is hacky and incomplete. Come up with a better solution.
if (fn instanceof LengthFunction) {
checkArgType(node.getChild(0), ListType.of(AnyType.getInstance()), node);
} else if (fn instanceof ParseIntFunction) {
checkArgType(node.getChild(0), StringType.getInstance(), node);
} else if (fn instanceof ParseFloatFunction) {
checkArgType(node.getChild(0), StringType.getInstance(), node);
}
}
private void checkNumArgs(SoyFunction function, FunctionNode node) {
int numArgs = node.numChildren();
Set<Integer> arities = function.getValidArgsSizes();
if (!arities.contains(numArgs)) {
errorReporter.report(
node.getSourceLocation(),
INCORRECT_NUM_ARGS,
function.getName(),
numArgs,
Joiner.on(" or ").join(arities));
}
}
/** @param fn The function that must take a loop variable. */
private void requireLoopVariableInScope(FunctionNode fn, ExprNode loopVariable) {
if (!(loopVariable instanceof VarRefNode
&& ((VarRefNode) loopVariable).getDefnDecl() instanceof LoopVar)) {
errorReporter.report(
fn.getSourceLocation(), LOOP_VARIABLE_NOT_IN_SCOPE, fn.getFunctionName());
}
}
private void checkArgType(ExprNode arg, SoyType expectedType, FunctionNode node) {
SoyType.Kind argTypeKind = arg.getType().getKind();
if (argTypeKind == SoyType.Kind.UNKNOWN || argTypeKind == SoyType.Kind.ERROR) {
return;
}
if (!expectedType.isAssignableFrom(arg.getType())) {
errorReporter.report(
arg.getSourceLocation(),
INCORRECT_ARG_TYPE,
node.getSoyFunction().getName(),
arg.getType(),
expectedType);
}
}
}
}