/*
* Copyright 2008 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.jssrc.internal;
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_FALSE;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_NULL;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_TRUE;
import static com.google.template.soy.jssrc.dsl.CodeChunk.arrayLiteral;
import static com.google.template.soy.jssrc.dsl.CodeChunk.dontTrustPrecedenceOf;
import static com.google.template.soy.jssrc.dsl.CodeChunk.id;
import static com.google.template.soy.jssrc.dsl.CodeChunk.mapLiteral;
import static com.google.template.soy.jssrc.dsl.CodeChunk.new_;
import static com.google.template.soy.jssrc.dsl.CodeChunk.number;
import static com.google.template.soy.jssrc.dsl.CodeChunk.operation;
import static com.google.template.soy.jssrc.dsl.CodeChunk.stringLiteral;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_ARRAY_MAP;
import static com.google.template.soy.jssrc.internal.JsRuntime.OPT_DATA;
import static com.google.template.soy.jssrc.internal.JsRuntime.OPT_IJ_DATA;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_CHECK_MAP_KEY;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_CHECK_NOT_NULL;
import static com.google.template.soy.jssrc.internal.JsRuntime.extensionField;
import static com.google.template.soy.jssrc.internal.JsRuntime.protoConstructor;
import static com.google.template.soy.jssrc.internal.JsRuntime.protoToSanitizedContentConverterFunction;
import static com.google.template.soy.jssrc.internal.JsRuntime.sanitizedContentToProtoConverterFunction;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor;
import com.google.template.soy.exprtree.BooleanNode;
import com.google.template.soy.exprtree.DataAccessNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.OperatorNode;
import com.google.template.soy.exprtree.ExprNode.PrimitiveNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FieldAccessNode;
import com.google.template.soy.exprtree.FloatNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.GlobalNode;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.ItemAccessNode;
import com.google.template.soy.exprtree.ListLiteralNode;
import com.google.template.soy.exprtree.MapLiteralNode;
import com.google.template.soy.exprtree.NullNode;
import com.google.template.soy.exprtree.OperatorNodes.AndOpNode;
import com.google.template.soy.exprtree.OperatorNodes.ConditionalOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NullCoalescingOpNode;
import com.google.template.soy.exprtree.OperatorNodes.OrOpNode;
import com.google.template.soy.exprtree.ProtoInitNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.dsl.CodeChunk.RequiresCollector;
import com.google.template.soy.jssrc.dsl.CodeChunk.WithValue;
import com.google.template.soy.jssrc.dsl.GoogRequire;
import com.google.template.soy.jssrc.internal.NullSafeAccumulator.FieldAccess;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.jssrc.restricted.SoyJsSrcFunction;
import com.google.template.soy.jssrc.restricted.SoyLibraryAssistedJsSrcFunction;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.aggregate.UnionType;
import com.google.template.soy.types.proto.Protos;
import com.google.template.soy.types.proto.SoyProtoType;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Visitor for translating a Soy expression (in the form of an {@code ExprNode}) into an
* equivalent chunk of JavaScript code.
*
* <p> Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <hr>
*
* <h3>Types and Dependencies</h3>
*
* Types are used to allow reflective access to protobuf values even after JSCompiler has
* rewritten field names.
*
* <p>
* For example, one might normally access field foo on a protocol buffer by calling
* <pre>my_pb.getFoo()</pre>
* A Soy author can access the same by writing
* <pre>{$my_pb.foo}</pre>
* But the relationship between "foo" and "getFoo" is not preserved by JSCompiler's renamer.
*
* <p>
* To avoid adding many spurious dependencies on all protocol buffers compiled with a Soy
* template, we make type-unsound (see CAVEAT below) assumptions:
* <ul>
* <li>That the top-level inputs, opt_data and opt_ijData, do not need conversion.
* <li>That we can enumerate the concrete types of a container when the default
* field lookup strategy would fail. For example, if an instance of {@code my.Proto}
* can be passed to the param {@code $my_pb}, then {@code $my_pb}'s static type is a
* super-type of {@code my.Proto}.</li>
* <li>That the template contains enough information to determine types that need to be
* converted.
* <br>
* Pluggable {@link com.google.template.soy.types.SoyTypeRegistry SoyTypeRegistries}
* allow recognizing input coercion, for example between {@code goog.html.type.SafeHtml}
* and Soy's {@code html} string sub-type.
* <br>
* When the converted type is a protocol-buffer type, we assume that the expression to be
* converted can be fully-typed by expressionTypesVisitor.
* </ul>
*
* <p>
* CAVEAT: These assumptions are unsound, but necessary to be able to deploy JavaScript
* binaries of acceptable size.
* <p>
* Type-failures are correctness issues but do not lead to increased exposure to XSS or
* otherwise compromise security or privacy since a failure to unpack a type leads to a
* value that coerces to a trivial value like {@code undefined} or {@code "[Object]"}.
* </p>
*
*/
public class TranslateExprNodeVisitor
extends AbstractReturningExprNodeVisitor<CodeChunk.WithValue> {
private static final Joiner COMMA_JOINER = Joiner.on(", ");
private static final SoyErrorKind CONSTANT_USED_AS_KEY_IN_MAP_LITERAL =
SoyErrorKind.of("Keys in map literals cannot be constants (found constant ''{0}'').");
private static final SoyErrorKind EXPR_IN_MAP_LITERAL_REQUIRES_QUOTE_KEYS_IF_JS =
SoyErrorKind.of("Expression key ''{0}'' in map literal must be wrapped in quoteKeysIfJs().");
private static final SoyErrorKind MAP_LITERAL_WITH_NON_ID_KEY_REQUIRES_QUOTE_KEYS_IF_JS =
SoyErrorKind.of(
"Map literal with non-identifier key {0} must be wrapped in quoteKeysIfJs().");
private static final SoyErrorKind UNION_ACCESSOR_MISMATCH =
SoyErrorKind.of(
"Cannot access field ''{0}'' of type ''{1}'', "
+ "because the different union member types have different access methods.");
/**
* Injectable factory for creating an instance of this class.
*/
public interface TranslateExprNodeVisitorFactory {
TranslateExprNodeVisitor create(
TranslationContext translationContext, ErrorReporter errorReporter);
}
/** The options for generating JS source code. */
private final SoyJsSrcOptions jsSrcOptions;
/**
* The current replacement JS expressions for the local variables (and foreach-loop special
* functions) current in scope.
*/
private final SoyToJsVariableMappings variableMappings;
private final ErrorReporter errorReporter;
private final CodeChunk.Generator codeGenerator;
@AssistedInject
TranslateExprNodeVisitor(
SoyJsSrcOptions jsSrcOptions,
@Assisted TranslationContext translationContext,
@Assisted ErrorReporter errorReporter) {
this.jsSrcOptions = jsSrcOptions;
this.errorReporter = errorReporter;
this.variableMappings = translationContext.soyToJsVariableMappings();
this.codeGenerator = translationContext.codeGenerator();
}
/**
* Method that returns code to access a named parameter.
* @param paramName the name of the parameter.
* @param isInjected true if this is an injected parameter.
* @return The code to access the value of that parameter.
*/
static CodeChunk.WithValue genCodeForParamAccess(String paramName, boolean isInjected) {
return isInjected ? OPT_IJ_DATA.dotAccess(paramName) : OPT_DATA.dotAccess(paramName);
}
@Override protected CodeChunk.WithValue visitExprRootNode(ExprRootNode node) {
// ExprRootNode is some indirection to make it easier to replace expressions. All we need to do
// is visit the only child
return visit(node.getRoot());
}
// -----------------------------------------------------------------------------------------------
// Implementations for primitives.
@Override
protected WithValue visitBooleanNode(BooleanNode node) {
return node.getValue() ? LITERAL_TRUE : LITERAL_FALSE;
}
@Override
protected WithValue visitFloatNode(FloatNode node) {
return number(node.getValue());
}
@Override
protected WithValue visitIntegerNode(IntegerNode node) {
return number(node.getValue());
}
@Override
protected WithValue visitNullNode(NullNode node) {
return LITERAL_NULL;
}
@Override protected CodeChunk.WithValue visitStringNode(StringNode node) {
return stringLiteral(node.getValue());
}
// -----------------------------------------------------------------------------------------------
// Implementations for collections.
@Override protected CodeChunk.WithValue visitListLiteralNode(ListLiteralNode node) {
return arrayLiteral(visitChildren(node));
}
@Override protected CodeChunk.WithValue visitMapLiteralNode(MapLiteralNode node) {
return visitMapLiteralNodeHelper(node, false);
}
/**
* Helper to visit a MapLiteralNode, with the extra option of whether to quote keys.
*/
private CodeChunk.WithValue visitMapLiteralNodeHelper(MapLiteralNode node, boolean doQuoteKeys) {
// If there are only string keys, then the expression will be
// {aa: 11, bb: 22} or {'aa': 11, 'bb': 22}
// where the former is with unquoted keys and the latter with quoted keys.
// If there are both string and nonstring keys, then the expression will be
// (function() { var $$tmp0 = {'aa': 11}; $$tmp0[opt_data.bb] = 22; return $$tmp0; })()
// If we are outputting JS code to be processed by Closure Compiler, then it is important that
// any unquoted map literal keys are string literals, since Closure Compiler can rename unquoted
// map keys and we want everything to be renamed at the same time.
boolean isProbablyUsingClosureCompiler =
jsSrcOptions.shouldGenerateJsdoc() ||
jsSrcOptions.shouldProvideRequireSoyNamespaces() ||
jsSrcOptions.shouldProvideRequireJsFunctions();
// We will divide the map literal contents into two categories.
//
// Key-value pairs with StringNode keys can be included in the JS object literal.
// Key-value pairs that are not StringNodes (VarRefs, IJ values, etc.) must be passed through
// the soy.$$checkMapKey() function, cannot be included in the JS object literal, and must
// generate code in the form of: $$map[soy.$$checkMapKey(key)] = value
LinkedHashMap<CodeChunk.WithValue, CodeChunk.WithValue> objLiteral = new LinkedHashMap<>();
LinkedHashMap<CodeChunk.WithValue, CodeChunk.WithValue> assignments = new LinkedHashMap<>();
// Process children
for (int i = 0; i < node.numChildren(); i += 2) {
ExprNode keyNode = node.getChild(i);
ExprNode valueNode = node.getChild(i + 1);
// error case: key is a non-string primitive.
// TODO: Support map literal with nonstring key. We can probably just remove this case and
// roll it into the next case.
if (!(keyNode instanceof StringNode) && keyNode instanceof PrimitiveNode) {
errorReporter.report(
keyNode.getSourceLocation(),
CONSTANT_USED_AS_KEY_IN_MAP_LITERAL,
keyNode.toSourceString());
continue;
}
// error case: for closure compiler users, do not allow unquoted, non-string-literal keys,
// since the compiler may change the names of any unquoted map keys.
if (isProbablyUsingClosureCompiler && !doQuoteKeys && !(keyNode instanceof StringNode)) {
errorReporter.report(
keyNode.getSourceLocation(),
EXPR_IN_MAP_LITERAL_REQUIRES_QUOTE_KEYS_IF_JS,
keyNode.toSourceString());
continue;
}
if (keyNode instanceof StringNode) {
// key is a StringNode; key-value pair gets included in the JS object literal.
// figure out whether the string should be quoted or not.
if (doQuoteKeys) {
objLiteral.put(visit(keyNode), visit(valueNode));
} else {
String strKey = ((StringNode) keyNode).getValue();
if (BaseUtils.isIdentifier(strKey)) {
objLiteral.put(id(strKey), visit(valueNode));
} else if (isProbablyUsingClosureCompiler) {
errorReporter.report(
keyNode.getSourceLocation(),
MAP_LITERAL_WITH_NON_ID_KEY_REQUIRES_QUOTE_KEYS_IF_JS,
keyNode.toSourceString());
continue;
} else {
objLiteral.put(visit(keyNode), visit(valueNode));
}
}
} else {
// key is not a StringNode; key must be passed through soy.$$checkMapKey() and the pair
// cannot be included in the JS object literal.
CodeChunk.WithValue rawKey = visit(keyNode);
assignments.put(SOY_CHECK_MAP_KEY.call(rawKey), visit(valueNode));
}
}
// Build the map literal
ImmutableList<CodeChunk.WithValue> keys = ImmutableList.copyOf(objLiteral.keySet());
ImmutableList<CodeChunk.WithValue> values = ImmutableList.copyOf(objLiteral.values());
CodeChunk.WithValue map = mapLiteral(keys, values);
if (assignments.isEmpty()) {
// If there are no assignments, we can return the map literal directly without assigning
// to a tmp var.
return map;
}
// Otherwise, we need to bail to a tmp var and emit assignment statements.
CodeChunk.WithValue mapVar = codeGenerator.declare(map).ref();
ImmutableList.Builder<CodeChunk> initialStatements = ImmutableList.builder();
for (Map.Entry<CodeChunk.WithValue, CodeChunk.WithValue> entry : assignments.entrySet()) {
initialStatements.add(mapVar.bracketAccess(entry.getKey()).assign(entry.getValue()));
}
return mapVar.withInitialStatements(initialStatements.build());
}
// -----------------------------------------------------------------------------------------------
// Implementations for data references.
@Override
protected CodeChunk.WithValue visitVarRefNode(VarRefNode node) {
CodeChunk.WithValue translation;
if (node.isDollarSignIjParameter()) {
// Case 1: Injected data reference.
return OPT_IJ_DATA.dotAccess(node.getName());
} else if ((translation = variableMappings.maybeGet(node.getName())) != null) {
// Case 2: In-scope local var.
return translation;
} else {
// Case 3: Data reference.
return genCodeForParamAccess(node.getName(), node.getDefnDecl().isInjected());
}
}
@Override
protected CodeChunk.WithValue visitDataAccessNode(DataAccessNode node) {
return visitNullSafeNode(node).result(codeGenerator);
}
/** See {@link NullSafeAccumulator} for discussion. */
private NullSafeAccumulator visitNullSafeNode(ExprNode node) {
switch (node.getKind()) {
case FIELD_ACCESS_NODE:
FieldAccessNode fieldAccess = (FieldAccessNode) node;
NullSafeAccumulator base = visitNullSafeNode(fieldAccess.getBaseExprChild());
FieldAccess access =
genCodeForFieldAccess(
fieldAccess.getBaseExprChild().getType(), fieldAccess, fieldAccess.getFieldName());
return base.dotAccess(access, fieldAccess.isNullSafe());
case ITEM_ACCESS_NODE:
ItemAccessNode itemAccess = (ItemAccessNode) node;
base = visitNullSafeNode(itemAccess.getBaseExprChild());
CodeChunk.WithValue key = visit(itemAccess.getKeyExprChild());
return base.bracketAccess(key, itemAccess.isNullSafe());
default:
return new NullSafeAccumulator(visit(node));
}
}
/**
* Generates the code for a field access, e.g. {@code .foo} or {@code .getFoo()}.
*
* @param baseType The type of the object that contains the field.
* @param fieldAccessNode The field access node.
* @param fieldName The field name.
*/
private FieldAccess genCodeForFieldAccess(
SoyType baseType, FieldAccessNode fieldAccessNode, String fieldName) {
Preconditions.checkNotNull(baseType);
// For unions, attempt to generate the field access code for each member
// type, and then see if they all agree.
if (baseType.getKind() == SoyType.Kind.UNION) {
// TODO(msamuel): We will need to generate fallback code for each variant.
UnionType unionType = (UnionType) baseType;
FieldAccess fieldAccess = null;
for (SoyType memberType : unionType.getMembers()) {
if (memberType.getKind() != SoyType.Kind.NULL) {
FieldAccess fieldAccessForType =
genCodeForFieldAccess(memberType, fieldAccessNode, fieldName);
if (fieldAccess == null) {
fieldAccess = fieldAccessForType;
} else if (!fieldAccess.equals(fieldAccessForType)) {
errorReporter.report(
fieldAccessNode.getSourceLocation(), UNION_ACCESSOR_MISMATCH, fieldName, baseType);
}
}
}
return fieldAccess;
}
if (baseType.getKind() == SoyType.Kind.PROTO) {
return genCodeForProtoAccess((SoyProtoType) baseType, fieldName);
}
return FieldAccess.id(fieldName);
}
private static FieldAccess genCodeForProtoAccess(SoyProtoType type, String fieldName) {
FieldDescriptor desc = type.getFieldDescriptor(fieldName);
boolean isSanitizedContent = Protos.isSanitizedContentField(desc);
Preconditions.checkNotNull(
desc,
"Error in proto %s, field not found: %s",
type.getDescriptor().getFullName(),
fieldName);
if (desc.isExtension()) {
return isSanitizedContent
? FieldAccess.callAndUnpack()
.getter("getExtension")
.arg(extensionField(desc))
.unpackFunctionName(protoToSanitizedContentConverterFunction(desc.getMessageType()))
.isRepeated(desc.isRepeated())
.build()
: FieldAccess.call("getExtension", extensionField(desc));
}
String getter = "get" + LOWER_CAMEL.to(UPPER_CAMEL, fieldName);
return isSanitizedContent
? FieldAccess.callAndUnpack()
.getter(getter)
.unpackFunctionName(protoToSanitizedContentConverterFunction(desc.getMessageType()))
.isRepeated(desc.isRepeated())
.build()
: FieldAccess.call(getter);
}
@Override protected CodeChunk.WithValue visitGlobalNode(GlobalNode node) {
if (node.isResolved()) {
return visit(node.getValue());
}
// jssrc supports unknown globals by plopping the global name directly into the output
// NOTE: this may cause the jscompiler to emit warnings, users will need to whitelist them or
// fix their use of unknown globals.
return CodeChunk.dottedIdNoRequire(node.getName());
}
// -----------------------------------------------------------------------------------------------
// Implementations for operators.
@Override protected CodeChunk.WithValue visitNullCoalescingOpNode(NullCoalescingOpNode node) {
List<CodeChunk.WithValue> operands = visitChildren(node);
CodeChunk.WithValue consequent = operands.get(0);
CodeChunk.WithValue alternate = operands.get(1);
// TODO(user): use the CodeChunk DSL to get variable initialization for free:
// return codeGenerator
// .newChunk()
// .assign((CodeChunk.WithValue) consequent)
// .if_(
// codeGenerator.expr(CodeChunk.THIS, " == null"),
// codeGenerator.expr(CodeChunk.THIS, " = ", alternate, ";"))
// .endif()
// .build();
// We can't do this yet because that chunk is never representable as a single expression.
return CodeChunk.ifExpression(id("$$temp").assign(consequent).doubleEqualsNull(), alternate)
.else_(id("$$temp"))
.build(codeGenerator);
}
@Override
protected CodeChunk.WithValue visitAndOpNode(AndOpNode node) {
Preconditions.checkArgument(node.numChildren() == 2);
return visit(node.getChild(0)).and(visit(node.getChild(1)), codeGenerator);
}
@Override
protected CodeChunk.WithValue visitOrOpNode(OrOpNode node) {
Preconditions.checkArgument(node.numChildren() == 2);
return visit(node.getChild(0)).or(visit(node.getChild(1)), codeGenerator);
}
@Override
protected WithValue visitConditionalOpNode(ConditionalOpNode node) {
Preconditions.checkArgument(node.numChildren() == 3);
return codeGenerator.conditionalExpression(
visit(node.getChild(0)), visit(node.getChild(1)), visit(node.getChild(2)));
}
@Override
protected CodeChunk.WithValue visitOperatorNode(OperatorNode node) {
return operation(node.getOperator(), visitChildren(node));
}
@Override
protected CodeChunk.WithValue visitProtoInitNode(ProtoInitNode node) {
SoyProtoType type = (SoyProtoType) node.getType();
CodeChunk.WithValue proto = new_(protoConstructor(type)).call();
if (node.numChildren() == 0) {
// If there's no further structure to the proto, no need to declare a variable.
return proto;
}
CodeChunk.WithValue protoVar = codeGenerator.declare(proto).ref();
ImmutableList.Builder<CodeChunk> initialStatements = ImmutableList.builder();
for (int i = 0; i < node.numChildren(); i++) {
String fieldName = node.getParamName(i);
FieldDescriptor fieldDesc = type.getFieldDescriptor(fieldName);
CodeChunk.WithValue fieldValue = visit(node.getChild(i));
if (Protos.isSanitizedContentField(fieldDesc)) {
CodeChunk.WithValue sanitizedContentPackFn =
sanitizedContentToProtoConverterFunction(fieldDesc.getMessageType());
fieldValue =
fieldDesc.isRepeated()
? GOOG_ARRAY_MAP.call(fieldValue, sanitizedContentPackFn)
: sanitizedContentPackFn.call(fieldValue);
}
// See go/jspb for setter and getter names. // MOE: strip_line
if (fieldDesc.isExtension()) {
CodeChunk.WithValue extInfo = extensionField(fieldDesc);
initialStatements.add(protoVar.dotAccess("setExtension").call(extInfo, fieldValue));
} else {
String setFn = "set" + LOWER_CAMEL.to(UPPER_CAMEL, fieldName);
initialStatements.add(protoVar.dotAccess(setFn).call(fieldValue));
}
}
return protoVar.withInitialStatements(initialStatements.build());
}
// -----------------------------------------------------------------------------------------------
// Implementations for functions.
@Override
protected CodeChunk.WithValue visitFunctionNode(FunctionNode node) {
SoyFunction soyFunction = node.getSoyFunction();
// TODO(user): Eliminate this case
if (soyFunction == null
|| !(soyFunction instanceof BuiltinFunction || soyFunction instanceof SoyJsSrcFunction)) {
// No function found. This is a v1 expression, only possible if allowDeprecatedSyntax == true
soyFunction = getUnknownFunction(node.getFunctionName(), node.numChildren());
}
if (soyFunction instanceof BuiltinFunction) {
switch ((BuiltinFunction) soyFunction) {
case IS_FIRST:
return visitIsFirstFunction(node);
case IS_LAST:
return visitIsLastFunction(node);
case INDEX:
return visitIndexFunction(node);
case QUOTE_KEYS_IF_JS:
return visitMapLiteralNodeHelper((MapLiteralNode) node.getChild(0), true);
case CHECK_NOT_NULL:
return visitCheckNotNullFunction(node);
case V1_EXPRESSION:
return visitV1ExpressionFunction(node);
default:
throw new AssertionError();
}
} else if (soyFunction instanceof SoyJsSrcFunction) {
List<CodeChunk.WithValue> args = visitChildren(node);
List<JsExpr> functionInputs = new ArrayList<>(args.size());
List<CodeChunk> initialStatements = new ArrayList<>();
RequiresCollector.IntoImmutableSet collector = new RequiresCollector.IntoImmutableSet();
// SoyJsSrcFunction doesn't understand CodeChunks; it needs JsExprs.
// Grab the JsExpr for each CodeChunk arg to deliver to the SoyToJsSrcFunction as input.
for (CodeChunk.WithValue arg : args) {
arg.collectRequires(collector);
functionInputs.add(arg.singleExprOrName());
Iterables.addAll(initialStatements, arg.initialStatements());
}
// Compute the function on the JsExpr inputs.
SoyJsSrcFunction soyJsSrcFunction = (SoyJsSrcFunction) soyFunction;
if (soyJsSrcFunction instanceof SoyLibraryAssistedJsSrcFunction) {
for (String name :
((SoyLibraryAssistedJsSrcFunction) soyJsSrcFunction).getRequiredJsLibNames()) {
collector.add(GoogRequire.create(name));
}
}
CodeChunk.WithValue functionOutput =
dontTrustPrecedenceOf(soyJsSrcFunction.computeForJsSrc(functionInputs), collector.get());
return functionOutput.withInitialStatements(initialStatements);
} else {
throw new AssertionError();
}
}
private CodeChunk.WithValue visitCheckNotNullFunction(FunctionNode node) {
return SOY_CHECK_NOT_NULL.call(visit(node.getChild(0)));
}
private CodeChunk.WithValue visitIsFirstFunction(FunctionNode node) {
String varName = ((VarRefNode) node.getChild(0)).getName();
return variableMappings.get(varName + "__isFirst");
}
private CodeChunk.WithValue visitIsLastFunction(FunctionNode node) {
String varName = ((VarRefNode) node.getChild(0)).getName();
return variableMappings.get(varName + "__isLast");
}
private CodeChunk.WithValue visitIndexFunction(FunctionNode node) {
String varName = ((VarRefNode) node.getChild(0)).getName();
return variableMappings.get(varName + "__index");
}
private CodeChunk.WithValue visitV1ExpressionFunction(FunctionNode node) {
String exprText = ((StringNode) node.getChild(0)).getValue();
return CodeChunk.fromExpr(
V1JsExprTranslator.translateToJsExpr(
exprText, node.getSourceLocation(), variableMappings, errorReporter),
ImmutableList.<GoogRequire>of());
}
private static SoyJsSrcFunction getUnknownFunction(final String name, final int argSize) {
return new SoyJsSrcFunction() {
@Override
public JsExpr computeForJsSrc(List<JsExpr> args) {
List<String> argStrings = new ArrayList<>();
for (JsExpr arg : args) {
argStrings.add(arg.getText());
}
return new JsExpr(name + "(" + COMMA_JOINER.join(argStrings) + ")", Integer.MAX_VALUE);
}
@Override
public String getName() {
return name;
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(argSize);
}
};
}
}