/*
* 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.Preconditions.checkNotNull;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.EMPTY_OBJECT_LITERAL;
import static com.google.template.soy.jssrc.dsl.CodeChunk.assign;
import static com.google.template.soy.jssrc.dsl.CodeChunk.declare;
import static com.google.template.soy.jssrc.dsl.CodeChunk.dottedIdNoRequire;
import static com.google.template.soy.jssrc.dsl.CodeChunk.forLoop;
import static com.google.template.soy.jssrc.dsl.CodeChunk.id;
import static com.google.template.soy.jssrc.dsl.CodeChunk.ifStatement;
import static com.google.template.soy.jssrc.dsl.CodeChunk.number;
import static com.google.template.soy.jssrc.dsl.CodeChunk.return_;
import static com.google.template.soy.jssrc.dsl.CodeChunk.stringLiteral;
import static com.google.template.soy.jssrc.dsl.CodeChunk.switch_;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_DEBUG;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_IS_OBJECT;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_REQUIRE;
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_ASSERTS_ASSERT_TYPE;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_GET_DELTEMPLATE_ID;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_REGISTER_DELEGATE_FN;
import static com.google.template.soy.jssrc.internal.JsRuntime.WINDOW_CONSOLE_LOG;
import static com.google.template.soy.jssrc.internal.JsRuntime.sanitizedContentOrdainerFunction;
import static com.google.template.soy.jssrc.internal.JsRuntime.sanitizedContentOrdainerFunctionForInternalBlocks;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.base.internal.UniqueNameGenerator;
import com.google.template.soy.data.internalutils.NodeContentKinds;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.Operator;
import com.google.template.soy.exprtree.OperatorNodes.NullCoalescingOpNode;
import com.google.template.soy.exprtree.VarDefn;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.html.AbstractHtmlSoyNodeVisitor;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.dsl.CodeChunkUtils;
import com.google.template.soy.jssrc.dsl.ConditionalBuilder;
import com.google.template.soy.jssrc.dsl.GoogRequire;
import com.google.template.soy.jssrc.dsl.SwitchBuilder;
import com.google.template.soy.jssrc.internal.GenJsExprsVisitor.GenJsExprsVisitorFactory;
import com.google.template.soy.parsepasses.contextautoesc.ContentSecurityPolicyPass;
import com.google.template.soy.passes.FindIndirectParamsVisitor;
import com.google.template.soy.passes.FindIndirectParamsVisitor.IndirectParamsInfo;
import com.google.template.soy.passes.ShouldEnsureDataIsDefinedVisitor;
import com.google.template.soy.shared.internal.FindCalleesNotInFileVisitor;
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.DebuggerNode;
import com.google.template.soy.soytree.ForNode;
import com.google.template.soy.soytree.ForNode.RangeArgs;
import com.google.template.soy.soytree.ForeachNode;
import com.google.template.soy.soytree.ForeachNonemptyNode;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfElseNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.LetValueNode;
import com.google.template.soy.soytree.LogNode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgHtmlTagNode;
import com.google.template.soy.soytree.PrintNode;
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.BlockNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.SwitchCaseNode;
import com.google.template.soy.soytree.SwitchDefaultNode;
import com.google.template.soy.soytree.SwitchNode;
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.Visibility;
import com.google.template.soy.soytree.defn.TemplateParam;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyTypeOps;
import com.google.template.soy.types.SoyTypes;
import com.google.template.soy.types.primitive.AnyType;
import com.google.template.soy.types.primitive.NullType;
import com.google.template.soy.types.primitive.StringType;
import com.google.template.soy.types.primitive.UnknownType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
/**
* Visitor for generating full JS code (i.e. statements) for parse tree nodes.
*
* <p> Precondition: MsgNode should not exist in the tree.
*
* <p> {@link #gen} should be called on a full parse tree. JS source code will be generated for
* all the Soy files. The return value is a list of strings, each string being the content of one
* generated JS file (corresponding to one Soy file).
*
*/
public class GenJsCodeVisitor extends AbstractHtmlSoyNodeVisitor<List<String>> {
/** Regex pattern to look for dots in a template name. */
private static final Pattern DOT = Pattern.compile("\\.");
/** The options for generating JS source code. */
protected final SoyJsSrcOptions jsSrcOptions;
/** Instance of JsExprTranslator to use. */
protected final JsExprTranslator jsExprTranslator;
/** Instance of DelTemplateNamer to use. */
private final DelTemplateNamer delTemplateNamer;
/** Instance of GenCallCodeUtils to use. */
protected final GenCallCodeUtils genCallCodeUtils;
/** The IsComputableAsJsExprsVisitor used by this instance. */
protected final IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor;
/** The CanInitOutputVarVisitor used by this instance. */
private final CanInitOutputVarVisitor canInitOutputVarVisitor;
/** Factory for creating an instance of GenJsExprsVisitor. */
private final GenJsExprsVisitorFactory genJsExprsVisitorFactory;
/** The contents of the generated JS files. */
private List<String> jsFilesContents;
/** The CodeBuilder to build the current JS file being generated (during a run). */
@VisibleForTesting JsCodeBuilder jsCodeBuilder;
/** The GenJsExprsVisitor used for the current template. */
protected GenJsExprsVisitor genJsExprsVisitor;
/** The assistant visitor for msgs used for the current template (lazily initialized). */
@VisibleForTesting GenJsCodeVisitorAssistantForMsgs assistantForMsgs;
protected TemplateRegistry templateRegistry;
/** Type operators. */
private final SoyTypeOps typeOps;
protected ErrorReporter errorReporter;
protected TranslationContext templateTranslationContext;
/**
* Used for looking up the local name for a given template call to a fully qualified template
* name. This is created on a per {@link SoyFileNode} basis.
*/
@VisibleForTesting protected TemplateAliases templateAliases;
@Inject
protected GenJsCodeVisitor(
SoyJsSrcOptions jsSrcOptions,
JsExprTranslator jsExprTranslator,
DelTemplateNamer delTemplateNamer,
GenCallCodeUtils genCallCodeUtils,
IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor,
CanInitOutputVarVisitor canInitOutputVarVisitor,
GenJsExprsVisitorFactory genJsExprsVisitorFactory,
SoyTypeOps typeOps) {
this.jsSrcOptions = jsSrcOptions;
this.jsExprTranslator = jsExprTranslator;
this.delTemplateNamer = delTemplateNamer;
this.genCallCodeUtils = genCallCodeUtils;
this.isComputableAsJsExprsVisitor = isComputableAsJsExprsVisitor;
this.canInitOutputVarVisitor = canInitOutputVarVisitor;
this.genJsExprsVisitorFactory = genJsExprsVisitorFactory;
this.typeOps = typeOps;
}
public List<String> gen(
SoyFileSetNode node, TemplateRegistry registry, ErrorReporter errorReporter) {
this.templateRegistry = checkNotNull(registry);
this.errorReporter = checkNotNull(errorReporter);
try {
jsFilesContents = new ArrayList<>();
jsCodeBuilder = null;
genJsExprsVisitor = null;
assistantForMsgs = null;
visit(node);
return jsFilesContents;
} finally {
this.templateRegistry = null;
this.errorReporter = null;
}
}
/** @deprecated Call {@link #gen} instead. */
@Override
@Deprecated
public final List<String> exec(SoyNode node) {
throw new UnsupportedOperationException();
}
/**
* This method must only be called by assistant visitors, in particular
* GenJsCodeVisitorAssistantForMsgs.
*/
public void visitForUseByAssistants(SoyNode node) {
visit(node);
}
/** TODO: tests should use {@link #gen} instead. */
@VisibleForTesting
void visitForTesting(
SoyNode node, ErrorReporter errorReporter) {
this.errorReporter = errorReporter;
visit(node);
}
@Override protected void visitChildren(ParentSoyNode<?> node) {
// If the block is empty or if the first child cannot initialize the output var, we must
// initialize the output var.
if (node.numChildren() == 0 || !canInitOutputVarVisitor.exec(node.getChild(0))) {
jsCodeBuilder.initOutputVarIfNecessary();
}
// For children that are computed by GenJsExprsVisitor, try to process as many of them as we can
// before adding to outputVar.
//
// output += 'a' + 'b';
// is preferable to
// output += 'a';
// output += 'b';
List<CodeChunk.WithValue> consecChunks = new ArrayList<>();
for (SoyNode child : node.getChildren()) {
if (isComputableAsJsExprsVisitor.exec(child)) {
consecChunks.addAll(genJsExprsVisitor.exec(child));
} else {
if (!consecChunks.isEmpty()) {
jsCodeBuilder.addChunksToOutputVar(consecChunks);
consecChunks.clear();
}
visit(child);
}
}
if (!consecChunks.isEmpty()) {
jsCodeBuilder.addChunksToOutputVar(consecChunks);
consecChunks.clear();
}
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override protected void visitSoyFileSetNode(SoyFileSetNode node) {
for (SoyFileNode soyFile : node.getChildren()) {
visit(soyFile);
}
}
/**
* @return A new CodeBuilder to create the contents of a file with.
*/
protected JsCodeBuilder createCodeBuilder() {
return new JsCodeBuilder();
}
/** @return A child CodeBuilder that inherits from the current builder. */
protected JsCodeBuilder createChildJsCodeBuilder() {
return new JsCodeBuilder(jsCodeBuilder);
}
/** @return The CodeBuilder used for generating file contents. */
protected JsCodeBuilder getJsCodeBuilder() {
return jsCodeBuilder;
}
/**
* Visits the given node, returning a {@link CodeChunk} encapsulating its JavaScript code. The
* chunk is indented one level from the current indent level.
*
* <p>Unlike {@link TranslateExprNodeVisitor}, GenJsCodeVisitor does not return anything as the
* result of visiting a subtree. To get recursive chunk-building, we use a hack, swapping out the
* {@link JsCodeBuilder} and using the unsound {@link
* CodeChunk#treatRawStringAsStatementLegacyOnly} API.
*/
private CodeChunk visitNodeReturningCodeChunk(ParentSoyNode<?> node) {
return doVisitReturningCodeChunk(node, false);
}
/**
* Visits the children of the given node, returning a {@link CodeChunk} encapsulating its
* JavaScript code. The chunk is indented one level from the current indent level.
*
* <p>This is needed to prevent infinite recursion when a visit() method needs to visit its
* children and return a CodeChunk.
*
* <p>Unlike {@link TranslateExprNodeVisitor}, GenJsCodeVisitor does not return anything as the
* result of visiting a subtree. To get recursive chunk-building, we use a hack, swapping out the
* {@link JsCodeBuilder} and using the unsound {@link
* CodeChunk#treatRawStringAsStatementLegacyOnly} API.
*/
private CodeChunk visitChildrenReturningCodeChunk(ParentSoyNode<?> node) {
return doVisitReturningCodeChunk(node, true);
}
/**
* Do not use directly; use {@link #visitChildrenReturningCodeChunk} or {@link
* #visitNodeReturningCodeChunk} instead.
*/
private CodeChunk doVisitReturningCodeChunk(SoyNode node, boolean visitChildren) {
// Replace jsCodeBuilder with a child JsCodeBuilder.
JsCodeBuilder original = jsCodeBuilder;
jsCodeBuilder = createChildJsCodeBuilder();
// Visit body.
jsCodeBuilder.increaseIndent();
if (visitChildren) {
visitChildren((ParentSoyNode<?>) node);
} else {
visit(node);
}
jsCodeBuilder.decreaseIndent();
CodeChunk chunk =
CodeChunk.treatRawStringAsStatementLegacyOnly(
jsCodeBuilder.getCode(), jsCodeBuilder.googRequires());
// Swap the original JsCodeBuilder back in, but preserve indent levels.
original.setIndent(jsCodeBuilder.getIndent());
jsCodeBuilder = original;
return chunk;
}
/**
* Example:
* <pre>
* // This file was automatically generated from my-templates.soy.
* // Please don't edit this file by hand.
*
* if (typeof boo == 'undefined') { var boo = {}; }
* if (typeof boo.foo == 'undefined') { boo.foo = {}; }
*
* ...
* </pre>
*/
@Override protected void visitSoyFileNode(SoyFileNode node) {
if (node.getSoyFileKind() != SoyFileKind.SRC) {
return; // don't generate code for deps
}
StringBuilder file = new StringBuilder();
file.append("// This file was automatically generated from ")
.append(node.getFileName())
.append(".\n");
file.append("// Please don't edit this file by hand.\n");
// Output a section containing optionally-parsed compiler directives in comments. Since these
// are comments, they are not controlled by an option, and will be removed by minifiers that do
// not understand them.
file.append("\n");
file.append("/**\n");
String fileOverviewDescription = " Templates in namespace " + node.getNamespace() + ".";
file.append(" * @fileoverview").append(fileOverviewDescription).append('\n');
if (node.getDelPackageName() != null) {
file.append(" * @modName {").append(node.getDelPackageName()).append("}\n");
}
addJsDocToProvideDelTemplates(file, node);
addJsDocToRequireDelTemplates(file, node);
addCodeToRequireCss(file, node);
file.append(" * @public\n").append(" */\n\n");
// Add code to define JS namespaces or add provide/require calls for Closure Library.
templateAliases = AliasUtils.IDENTITY_ALIASES;
jsCodeBuilder = createCodeBuilder();
if (jsSrcOptions.shouldGenerateGoogModules()) {
templateAliases = AliasUtils.createTemplateAliases(node);
addCodeToDeclareGoogModule(file, node);
addCodeToRequireGoogModules(node);
} else if (jsSrcOptions.shouldProvideRequireSoyNamespaces()) {
addCodeToProvideSoyNamespace(file, node);
if (jsSrcOptions.shouldProvideBothSoyNamespacesAndJsFunctions()) {
addCodeToProvideJsFunctions(file, node);
}
file.append('\n');
addCodeToRequireSoyNamespaces(node);
} else if (jsSrcOptions.shouldProvideRequireJsFunctions()) {
if (jsSrcOptions.shouldProvideBothSoyNamespacesAndJsFunctions()) {
addCodeToProvideSoyNamespace(file, node);
}
addCodeToProvideJsFunctions(file, node);
file.append('\n');
addCodeToRequireJsFunctions(node);
} else {
addCodeToDefineJsNamespaces(file, node);
}
// Add code for each template.
for (TemplateNode template : node.getChildren()) {
jsCodeBuilder.appendLine().appendLine();
visit(template);
}
if (jsSrcOptions.shouldProvideRequireSoyNamespaces()
|| jsSrcOptions.shouldProvideRequireJsFunctions()
|| jsSrcOptions.shouldGenerateGoogModules()) {
// if none of these options are set, the user must not be using the closure dependency system.
jsCodeBuilder.appendGoogRequires(file);
}
jsCodeBuilder.appendCode(file);
jsFilesContents.add(file.toString());
jsCodeBuilder = null;
}
/**
* Appends requirecss jsdoc tags in the file header section.
*
* @param soyFile The file with the templates..
*/
private static void addCodeToRequireCss(StringBuilder header, SoyFileNode soyFile) {
SortedSet<String> requiredCssNamespaces = new TreeSet<>();
requiredCssNamespaces.addAll(soyFile.getRequiredCssNamespaces());
for (TemplateNode template : soyFile.getChildren()) {
requiredCssNamespaces.addAll(template.getRequiredCssNamespaces());
}
// NOTE: CSS requires in JS can only be done on a file by file basis at this time. Perhaps in
// the future, this might be supported per function.
for (String requiredCssNamespace : requiredCssNamespaces) {
header.append(" * @requirecss {").append(requiredCssNamespace).append("}\n");
}
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to define JS namespaces.
*
* @param header
* @param soyFile The node we're visiting.
*/
private void addCodeToDefineJsNamespaces(StringBuilder header, SoyFileNode soyFile) {
SortedSet<String> jsNamespaces = new TreeSet<>();
for (TemplateNode template : soyFile.getChildren()) {
String templateName = template.getTemplateName();
Matcher dotMatcher = DOT.matcher(templateName);
while (dotMatcher.find()) {
jsNamespaces.add(templateName.substring(0, dotMatcher.start()));
}
}
for (String jsNamespace : jsNamespaces) {
boolean hasDot = jsNamespace.indexOf('.') >= 0;
// If this is a top level namespace and the option to declare top level
// namespaces is turned off, skip declaring it.
if (jsSrcOptions.shouldDeclareTopLevelNamespaces() || hasDot) {
header
.append("if (typeof ")
.append(jsNamespace)
.append(" == 'undefined') { ")
.append(hasDot ? "" : "var ")
.append(jsNamespace)
.append(" = {}; }\n");
}
}
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to provide Soy namespaces.
*
* @param header
* @param soyFile The node we're visiting.
*/
private static void addCodeToProvideSoyNamespace(StringBuilder header, SoyFileNode soyFile) {
header.append("goog.provide('").append(soyFile.getNamespace()).append("');\n");
}
/**
* @param soyNamespace The namespace as declared by the user.
* @return The namespace to import/export templates.
*/
protected String getGoogModuleNamespace(String soyNamespace) {
return soyNamespace;
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to generate a module definition.
*
* @param header
* @param soyFile The node we're visiting.
*/
private void addCodeToDeclareGoogModule(StringBuilder header, SoyFileNode soyFile) {
String exportNamespace = getGoogModuleNamespace(soyFile.getNamespace());
header.append("goog.module('").append(exportNamespace).append("');\n\n");
}
/**
* Generates the module imports and aliasing. This generates code like the following:
*
* <pre>
* var $import1 = goog.require('some.namespace');
* var $templateAlias1 = $import1.tmplOne;
* var $templateAlias2 = $import1.tmplTwo;
* var $import2 = goog.require('other.namespace');
* ...
* </pre>
*
* @param soyFile The node we're visiting.
*/
private void addCodeToRequireGoogModules(SoyFileNode soyFile) {
int counter = 1;
// Get all the unique calls in the file.
Set<String> calls = new HashSet<>();
for (CallBasicNode callNode : SoyTreeUtils.getAllNodesOfType(soyFile, CallBasicNode.class)) {
calls.add(callNode.getCalleeName());
}
// Map all the unique namespaces to the templates in those namespaces.
SetMultimap<String, String> namespaceToTemplates = TreeMultimap.create();
for (String call : calls) {
namespaceToTemplates.put(call.substring(0, call.lastIndexOf('.')), call);
}
for (String namespace : namespaceToTemplates.keySet()) {
// Skip the file's own namespace as there is nothing to import/alias.
if (namespace.equals(soyFile.getNamespace())) {
continue;
}
// Add a require of the module
String namespaceAlias = "$import" + counter++;
String importNamespace = getGoogModuleNamespace(namespace);
jsCodeBuilder.append(
declare(namespaceAlias, GOOG_REQUIRE.call(stringLiteral(importNamespace))));
// Alias all the templates used from the module
for (String fullyQualifiedName : namespaceToTemplates.get(namespace)) {
String alias = templateAliases.get(fullyQualifiedName);
String shortName = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf('.'));
jsCodeBuilder.append(declare(alias, dottedIdNoRequire(namespaceAlias + shortName)));
}
}
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to provide template JS functions.
*
* @param soyFile The node we're visiting.
*/
private static void addCodeToProvideJsFunctions(StringBuilder header, SoyFileNode soyFile) {
SortedSet<String> templateNames = new TreeSet<>();
for (TemplateNode template : soyFile.getChildren()) {
templateNames.add(template.getTemplateName());
}
for (String templateName : templateNames) {
header.append("goog.provide('").append(templateName).append("');\n");
}
}
private void addJsDocToProvideDelTemplates(StringBuilder header, SoyFileNode soyFile) {
SortedSet<String> delTemplateNames = new TreeSet<>();
for (TemplateNode template : soyFile.getChildren()) {
if (template instanceof TemplateDelegateNode) {
delTemplateNames.add(delTemplateNamer.getDelegateName((TemplateDelegateNode) template));
}
}
for (String delTemplateName : delTemplateNames) {
header.append(" * @hassoydeltemplate {").append(delTemplateName).append("}\n");
}
}
private void addJsDocToRequireDelTemplates(StringBuilder header, SoyFileNode soyFile) {
SortedSet<String> delTemplateNames = new TreeSet<>();
for (CallDelegateNode delCall :
SoyTreeUtils.getAllNodesOfType(soyFile, CallDelegateNode.class)) {
delTemplateNames.add(delTemplateNamer.getDelegateName(delCall));
}
for (String delTemplateName : delTemplateNames) {
header.append(" * @hassoydelcall {").append(delTemplateName).append("}\n");
}
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to require Soy namespaces.
* @param soyFile The node we're visiting.
*/
private void addCodeToRequireSoyNamespaces(SoyFileNode soyFile) {
String prevCalleeNamespace = null;
Set<String> calleeNamespaces = new TreeSet<>();
for (CallBasicNode node : new FindCalleesNotInFileVisitor().exec(soyFile)) {
String calleeNotInFile = node.getCalleeName();
int lastDotIndex = calleeNotInFile.lastIndexOf('.');
calleeNamespaces.add(calleeNotInFile.substring(0, lastDotIndex));
}
for (String calleeNamespace : calleeNamespaces) {
if (calleeNamespace.length() > 0 && !calleeNamespace.equals(prevCalleeNamespace)) {
jsCodeBuilder.addGoogRequire(GoogRequire.create(calleeNamespace));
prevCalleeNamespace = calleeNamespace;
}
}
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to require template JS functions.
* @param soyFile The node we're visiting.
*/
private void addCodeToRequireJsFunctions(SoyFileNode soyFile) {
for (CallBasicNode node : new FindCalleesNotInFileVisitor().exec(soyFile)) {
jsCodeBuilder.addGoogRequire(GoogRequire.create(node.getCalleeName()));
}
}
/**
* @param node The template node that is being generated
* @return The JavaScript type of the content generated by this template.
*/
protected String getTemplateReturnType(TemplateNode node) {
// For strict autoescaping templates, the result is actually a typesafe wrapper.
// We prepend "!" to indicate it is non-nullable.
return (node.getContentKind() == null)
? "string"
: "!" + NodeContentKinds.toJsSanitizedContentCtorName(node.getContentKind());
}
/**
* Outputs a {@link TemplateNode}, generating the function open and close, along with a a debug
* template name.
*
* <p>If aliasing is not performed (which is always the case for V1 templates), this looks like:
* <pre>
* my.namespace.func = function(opt_data, opt_sb) {
* ...
* };
* if (goog.DEBUG) {
* my.namespace.func.soyTemplateName = 'my.namespace.func';
* }
* </pre>
*
* <p>If aliasing is performed, this looks like:
* <pre>
* function $func(opt_data, opt_sb) {
* ...
* }
* exports.func = $func;
* if (goog.DEBUG) {
* $func.soyTemplateName = 'my.namespace.func';
* }
* <p>Note that the alias is not exactly the function name as in may conflict with a reserved
* JavaScript identifier.
* </pre>
*/
@Override protected void visitTemplateNode(TemplateNode node) {
boolean useStrongTyping = hasStrictParams(node);
String templateName = node.getTemplateName();
String partialName = node.getPartialTemplateName();
String alias;
boolean addToExports = jsSrcOptions.shouldGenerateGoogModules();
// TODO(lukes): does it make sense to add deltempaltes or private templates to exports?
if (addToExports && node instanceof TemplateDelegateNode) {
alias = node.getPartialTemplateName().substring(1);
} else {
alias = templateAliases.get(templateName);
}
// TODO(lukes): reserve all the namespace prefixes that are in scope
// TODO(lukes): use this for all local variable declarations
UniqueNameGenerator nameGenerator = JsSrcNameGenerators.forLocalVariables();
CodeChunk.Generator codeGenerator = CodeChunk.Generator.create(nameGenerator);
templateTranslationContext =
TranslationContext.of(
SoyToJsVariableMappings.forNewTemplate(), codeGenerator, nameGenerator);
genJsExprsVisitor =
genJsExprsVisitorFactory.create(templateTranslationContext, templateAliases, errorReporter);
assistantForMsgs = null;
String paramsRecordType = null;
// ------ Generate JS Doc. ------
if (jsSrcOptions.shouldGenerateJsdoc()) {
jsCodeBuilder.appendLine("/**");
jsCodeBuilder.append(" * @param {");
if (useStrongTyping) {
// TODO(lukes): use the typedef here
paramsRecordType = genParamsRecordType(node);
jsCodeBuilder.append(paramsRecordType);
} else {
jsCodeBuilder.append("Object<string, *>=");
}
jsCodeBuilder.appendLine("} opt_data");
jsCodeBuilder.appendLine(" * @param {Object<string, *>=} opt_ijData");
jsCodeBuilder.appendLine(" * @param {Object<string, *>=} opt_ijData_deprecated");
String returnType = getTemplateReturnType(node);
jsCodeBuilder.appendLine(" * @return {", returnType, "}");
String suppressions = "checkTypes";
jsCodeBuilder.appendLine(" * @suppress {" + suppressions + "}");
if (node.getVisibility() == Visibility.PRIVATE) {
jsCodeBuilder.appendLine(" * @private");
}
jsCodeBuilder.appendLine(" */");
}
// ------ Generate function definition up to opening brace. ------
if (addToExports) {
jsCodeBuilder.appendLine(
"function ", alias, "(opt_data, opt_ijData, opt_ijData_deprecated) {");
} else {
jsCodeBuilder.appendLine(alias, " = function(opt_data, opt_ijData, opt_ijData_deprecated) {");
}
jsCodeBuilder.increaseIndent();
jsCodeBuilder.appendLine("opt_ijData = opt_ijData_deprecated || opt_ijData;");
// If there are any null coalescing operators or switch nodes then we need to generate an
// additional temporary variable.
if (!SoyTreeUtils.getAllNodesOfType(node, NullCoalescingOpNode.class).isEmpty()
|| !SoyTreeUtils.getAllNodesOfType(node, SwitchNode.class).isEmpty()) {
jsCodeBuilder.appendLine("var $$temp;");
}
// Generate statement to ensure data is defined, if necessary.
if (new ShouldEnsureDataIsDefinedVisitor().exec(node)) {
jsCodeBuilder.append(assign("opt_data", OPT_DATA.or(EMPTY_OBJECT_LITERAL, codeGenerator)));
}
if (shouldEnsureIjDataIsDefined(node)) {
jsCodeBuilder.append(
assign("opt_ijData", OPT_IJ_DATA.or(EMPTY_OBJECT_LITERAL, codeGenerator)));
}
// ------ Generate function body. ------
generateFunctionBody(node);
// ------ Generate function closing brace and add to exports if necessary. ------
jsCodeBuilder.decreaseIndent();
if (addToExports) {
jsCodeBuilder.appendLine("}");
jsCodeBuilder.append(
assign("exports" /* partialName starts with a dot */ + partialName, id(alias)));
} else {
jsCodeBuilder.appendLine("};");
}
// ------ Add the @typedef of opt_data. ------
if (paramsRecordType != null) {
jsCodeBuilder.appendLine("/**");
jsCodeBuilder.appendLine(" * @typedef {", paramsRecordType, "}");
jsCodeBuilder.appendLine(" */");
jsCodeBuilder.appendLine(alias + ".Params;");
}
// ------ Add the fully qualified template name to the function to use in debug code. ------
jsCodeBuilder.append(
ifStatement(GOOG_DEBUG, assign(alias + ".soyTemplateName", stringLiteral(templateName)))
.build());
// ------ If delegate template, generate a statement to register it. ------
if (node instanceof TemplateDelegateNode) {
TemplateDelegateNode nodeAsDelTemplate = (TemplateDelegateNode) node;
jsCodeBuilder.append(
SOY_REGISTER_DELEGATE_FN.call(
SOY_GET_DELTEMPLATE_ID.call(
stringLiteral(delTemplateNamer.getDelegateName(nodeAsDelTemplate))),
stringLiteral(nodeAsDelTemplate.getDelTemplateVariant()),
number(nodeAsDelTemplate.getDelPriority().getValue()),
dottedIdNoRequire(alias)));
}
}
/**
* Returns true if the given template should ensure that the {@code opt_ijData} param is defined.
*
* <p>The current logic exists for CSP support which is enabled by default. CSP support works by
* generating references to an {@code $ij} param called {@code csp_nonce}, so to ensure that
* templates are compatible we only need to ensure the opt_ijData param is available is if the
* template references {@code $ij.csp_nonce}.
*/
private static boolean shouldEnsureIjDataIsDefined(TemplateNode node) {
for (VarRefNode ref : SoyTreeUtils.getAllNodesOfType(node, VarRefNode.class)) {
if (ref.isDollarSignIjParameter()) {
if (ref.getName().equals(ContentSecurityPolicyPass.CSP_NONCE_VARIABLE_NAME)) {
return true;
}
} else if (ref.getDefnDecl().isInjected() && ref.getDefnDecl().kind() == VarDefn.Kind.PARAM) {
// if it is an {@inject } param then we will generate unconditional type assertions that
// dereference opt_ijData. So there is no need to ensure it is defined.
return false;
}
}
return false;
}
/**
* Generates the function body.
*/
protected void generateFunctionBody(TemplateNode node) {
// Type check parameters.
genParamTypeChecks(node);
CodeChunk.WithValue templateBody;
if (isComputableAsJsExprsVisitor.exec(node)) {
// Case 1: The code style is 'concat' and the whole template body can be represented as JS
// expressions. We specially handle this case because we don't want to generate the variable
// 'output' at all. We simply concatenate the JS expressions and return the result.
List<CodeChunk.WithValue> templateBodyChunks = genJsExprsVisitor.exec(node);
if (node.getContentKind() == null) {
// The template is not strict. Thus, it may not apply an escaping directive to *every* print
// command, which means that some of its print commands could produce a number. Thus, there
// is a danger that a plus operator between two expressions in the list will do numeric
// addition instead of string concatenation. Furthermore, a non-strict template always needs
// to return a string, but if there is just one expression in the list, and we return it as
// is, we may not always produce a string (since an escaping directive may not be getting
// applied in that expression at all, or a directive might be getting applied that produces
// SanitizedContent). We thus call a method that makes sure to return an expression that
// produces a string and is in no danger of using numeric addition when concatenating the
// expressions in the list.
templateBody = CodeChunkUtils.concatChunksForceString(templateBodyChunks);
} else {
// The template is strict. Thus, it applies an escaping directive to *every* print command,
// which means that no print command produces a number, which means that there is no danger
// of a plus operator between two print commands doing numeric addition instead of string
// concatenation. And since a strict template needs to return SanitizedContent, it is ok to
// get an expression that produces SanitizedContent, which is indeed possible with an
// escaping directive that produces SanitizedContent. Thus, we do not have to be extra
// careful when concatenating the expressions in the list.
templateBody = CodeChunkUtils.concatChunks(templateBodyChunks);
}
} else {
// Case 2: Normal case.
jsCodeBuilder.pushOutputVar("output");
visitChildren(node);
templateBody = id("output");
jsCodeBuilder.popOutputVar();
}
if (node.getContentKind() != null) {
// Templates with autoescape="strict" return the SanitizedContent wrapper for its kind:
// - Call sites are wrapped in an escaper. Returning SanitizedContent prevents re-escaping.
// - The topmost call into Soy returns a SanitizedContent. This will make it easy to take
// the result of one template and feed it to another, and also to confidently assign
// sanitized HTML content to innerHTML. This does not use the internal-blocks variant,
// and so will wrap empty strings.
templateBody = sanitizedContentOrdainerFunction(node.getContentKind()).call(templateBody);
}
jsCodeBuilder.append(return_(templateBody));
}
protected GenJsCodeVisitorAssistantForMsgs getAssistantForMsgs() {
if (assistantForMsgs == null) {
assistantForMsgs =
new GenJsCodeVisitorAssistantForMsgs(
this /* master */,
jsSrcOptions,
jsExprTranslator,
genCallCodeUtils,
isComputableAsJsExprsVisitor,
templateAliases,
genJsExprsVisitor,
templateTranslationContext,
errorReporter);
}
return assistantForMsgs;
}
@Override protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
throw new AssertionError("Inconceivable! LetContentNode should catch this directly.");
}
@Override protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) {
throw new AssertionError();
}
@Override protected void visitPrintNode(PrintNode node) {
jsCodeBuilder.addChunksToOutputVar(genJsExprsVisitor.exec(node));
}
/**
* Example:
* <pre>
* {let $boo: $foo.goo[$moo] /}
* </pre>
* might generate
* <pre>
* var boo35 = opt_data.foo.goo[opt_data.moo];
* </pre>
*/
@Override protected void visitLetValueNode(LetValueNode node) {
String generatedVarName = node.getUniqueVarName();
// Generate code to define the local var.
CodeChunk.WithValue value = jsExprTranslator.translateToCodeChunk(
node.getValueExpr(), templateTranslationContext, errorReporter);
jsCodeBuilder.append(declare(generatedVarName, value));
// Add a mapping for generating future references to this local var.
templateTranslationContext
.soyToJsVariableMappings()
.put(
node.getVarName(),
id(generatedVarName));
}
/**
* Example:
* <pre>
* {let $boo}
* Hello {$name}
* {/let}
* </pre>
* might generate
* <pre>
* var boo35 = 'Hello ' + opt_data.name;
* </pre>
*/
@Override protected void visitLetContentNode(LetContentNode node) {
// Optimization: {msg} nodes emit statements and result in a JsExpr with a single variable. Use
// that variable (typically the MSG_* from getMsg) as-is instead of wrapping a new var around it
if (node.getChildren().size() == 1 && node.getChild(0) instanceof MsgFallbackGroupNode) {
String msgVar =
getAssistantForMsgs().generateMsgGroupVariable((MsgFallbackGroupNode) node.getChild(0));
templateTranslationContext.soyToJsVariableMappings().put(node.getVarName(), id(msgVar));
return;
}
String generatedVarName = node.getUniqueVarName();
CodeChunk.WithValue generatedVar = id(generatedVarName);
// Generate code to define the local var.
jsCodeBuilder.pushOutputVar(generatedVarName);
visitChildren(node);
jsCodeBuilder.popOutputVar();
if (node.getContentKind() != null) {
// If the let node had a content kind specified, it was autoescaped in the corresponding
// context. Hence the result of evaluating the let block is wrapped in a SanitizedContent
// instance of the appropriate kind.
// The expression for the constructor of SanitizedContent of the appropriate kind (e.g.,
// "soydata.VERY_UNSAFE.ordainSanitizedHtml"), or null if the node has no 'kind' attribute.
jsCodeBuilder.append(
assign(
generatedVarName,
sanitizedContentOrdainerFunctionForInternalBlocks(node.getContentKind())
.call(generatedVar)));
}
// Add a mapping for generating future references to this local var.
templateTranslationContext.soyToJsVariableMappings().put(node.getVarName(), generatedVar);
}
/**
* Example:
* <pre>
* {if $boo.foo > 0}
* ...
* {/if}
* </pre>
* might generate
* <pre>
* if (opt_data.boo.foo > 0) {
* ...
* }
* </pre>
*/
@Override protected void visitIfNode(IfNode node) {
if (isComputableAsJsExprsVisitor.exec(node)) {
jsCodeBuilder.addChunksToOutputVar(genJsExprsVisitor.exec(node));
} else {
generateNonExpressionIfNode(node);
}
}
/**
* Generates the JavaScript code for an {if} block that cannot be done as an expression.
*
* <p>TODO(user): Instead of interleaving JsCodeBuilders like this, consider refactoring
* GenJsCodeVisitor to return CodeChunks for each sub-Template level SoyNode.
*/
protected void generateNonExpressionIfNode(IfNode node) {
ConditionalBuilder conditional = null;
for (SoyNode child : node.getChildren()) {
if (child instanceof IfCondNode) {
IfCondNode condNode = (IfCondNode) child;
// Convert predicate.
CodeChunk.WithValue predicate =
jsExprTranslator.translateToCodeChunk(
condNode.getExpr(), templateTranslationContext, errorReporter);
// Convert body.
CodeChunk consequent = visitChildrenReturningCodeChunk(condNode);
// Add if-block to conditional.
if (conditional == null) {
conditional = ifStatement(predicate, consequent);
} else {
conditional.elseif_(predicate, consequent);
}
} else if (child instanceof IfElseNode) {
// Convert body.
CodeChunk trailingElse = visitChildrenReturningCodeChunk((IfElseNode) child);
// Add else-block to conditional.
conditional.else_(trailingElse);
} else {
throw new AssertionError();
}
}
jsCodeBuilder.append(conditional.build());
}
/**
* Example:
* <pre>
* {switch $boo}
* {case 0}
* ...
* {case 1, 2}
* ...
* {default}
* ...
* {/switch}
* </pre>
* might generate
* <pre>
* switch (opt_data.boo) {
* case 0:
* ...
* break;
* case 1:
* case 2:
* ...
* break;
* default:
* ...
* }
* </pre>
*/
@Override protected void visitSwitchNode(SwitchNode node) {
CodeChunk.WithValue switchOn = coerceTypeForSwitchComparison(node.getExpr());
SwitchBuilder switchBuilder = switch_(switchOn);
for (SoyNode child : node.getChildren()) {
if (child instanceof SwitchCaseNode) {
SwitchCaseNode scn = (SwitchCaseNode) child;
ImmutableList.Builder<CodeChunk.WithValue> caseChunks = ImmutableList.builder();
for (ExprNode caseExpr : scn.getExprList()) {
CodeChunk.WithValue caseChunk =
jsExprTranslator.translateToCodeChunk(
caseExpr, templateTranslationContext, errorReporter);
caseChunks.add(caseChunk);
}
CodeChunk body = visitChildrenReturningCodeChunk(scn);
switchBuilder.case_(caseChunks.build(), body);
} else if (child instanceof SwitchDefaultNode) {
CodeChunk body = visitChildrenReturningCodeChunk((SwitchDefaultNode) child);
switchBuilder.default_(body);
} else {
throw new AssertionError();
}
}
jsCodeBuilder.append(switchBuilder.build());
}
// js switch statements use === for comparing the switch expr to the cases. In order to preserve
// soy equality semantics for sanitized content objects we need to coerce cases and switch exprs
// to strings.
private CodeChunk.WithValue coerceTypeForSwitchComparison(ExprRootNode expr) {
CodeChunk.WithValue switchOn =
jsExprTranslator.translateToCodeChunk(expr, templateTranslationContext, errorReporter);
SoyType type = expr.getType();
// If the type is possibly a sanitized content type then we need to toString it.
if (SoyTypes.makeNullable(StringType.getInstance()).isAssignableFrom(type)
|| type.equals(AnyType.getInstance())
|| type.equals(UnknownType.getInstance())) {
CodeChunk.Generator codeGenerator = templateTranslationContext.codeGenerator();
CodeChunk.WithValue tmp = codeGenerator.declare(switchOn).ref();
return CodeChunk.ifExpression(GOOG_IS_OBJECT.call(tmp), tmp.dotAccess("toString").call())
.else_(tmp)
.build(codeGenerator);
}
// For everything else just pass through. switching on objects/collections is unlikely to
// have reasonably defined behavior.
return switchOn;
}
/**
* Example:
*
* <pre>
* {foreach $foo in $boo.foos}
* ...
* {ifempty}
* ...
* {/foreach}
* </pre>
*
* might generate
*
* <pre>
* var foo2List = opt_data.boo.foos;
* var foo2ListLen = foo2List.length;
* if (foo2ListLen > 0) {
* ...
* } else {
* ...
* }
* </pre>
*/
@Override
protected void visitForeachNode(ForeachNode node) {
boolean hasIfempty = (node.numChildren() == 2);
// Build some local variable names.
ForeachNonemptyNode nonEmptyNode = (ForeachNonemptyNode) node.getChild(0);
String varPrefix = nonEmptyNode.getVarName() + node.getId();
// TODO(user): A more consistent pattern for local variable management.
String listName = varPrefix + "List";
String limitName = varPrefix + "ListLen";
// Define list var and list-len var.
CodeChunk.WithValue dataRef =
jsExprTranslator.translateToCodeChunk(
node.getExpr(), templateTranslationContext, errorReporter);
jsCodeBuilder.append(declare(listName, dataRef));
jsCodeBuilder.append(declare(limitName, dottedIdNoRequire(listName + ".length")));
// Generate the foreach body as a CodeChunk.
CodeChunk foreachBody = visitNodeReturningCodeChunk(nonEmptyNode);
if (hasIfempty) {
// If there is an ifempty node, wrap the foreach body in an if statement and append the
// ifempty body as the else clause.
CodeChunk ifemptyBody = visitChildrenReturningCodeChunk(node.getChild(1));
CodeChunk.WithValue limitCheck = id(limitName).op(Operator.GREATER_THAN, number(0));
CodeChunk foreach = ifStatement(limitCheck, foreachBody).else_(ifemptyBody).build();
jsCodeBuilder.append(foreach);
} else {
// Otherwise, simply append the foreach body.
jsCodeBuilder.append(foreachBody);
}
}
/**
* Example:
*
* <pre>
* {foreach $foo in $boo.foos}
* ...
* {/foreach}
* </pre>
*
* might generate
*
* <pre>
* for (var foo2Index = 0; foo2Index < foo2ListLen; foo2Index++) {
* var foo2Data = foo2List[foo2Index];
* ...
* }
* </pre>
*/
@Override
protected void visitForeachNonemptyNode(ForeachNonemptyNode node) {
// Build some local variable names.
String varName = node.getVarName();
String varPrefix = varName + node.getForeachNodeId();
// TODO(user): A more consistent pattern for local variable management.
String listName = varPrefix + "List";
String loopIndexName = varPrefix + "Index";
String dataName = varPrefix + "Data";
String limitName = varPrefix + "ListLen";
CodeChunk.WithValue loopIndex = id(loopIndexName);
CodeChunk.WithValue limit = id(limitName);
// Populate the local var translations with the translations from this node.
templateTranslationContext
.soyToJsVariableMappings()
.put(varName, id(dataName))
.put(varName + "__isFirst", loopIndex.doubleEquals(number(0)))
.put(varName + "__isLast", loopIndex.doubleEquals(limit.minus(number(1))))
.put(varName + "__index", loopIndex);
// Generate the loop body.
CodeChunk data = declare(dataName, id(listName).bracketAccess(loopIndex));
CodeChunk foreachBody = visitChildrenReturningCodeChunk(node);
CodeChunk body = data.concat(foreachBody);
// Create the entire for block.
CodeChunk forChunk = forLoop(loopIndexName, limit, body);
// Do not call visitReturningCodeChunk(); This is already inside the one from visitForeachNode()
jsCodeBuilder.append(forChunk);
}
/**
* Example:
*
* <pre>
* {for $i in range(1, $boo, $goo)}
* ...
* {/for}
* </pre>
*
* might generate
*
* <pre>
* var i4Limit = opt_data.boo;
* var i4Increment = opt_data.goo
* for (var i4 = 1; i4 < i4Limit; i4 += i4Increment) {
* ...
* }
* </pre>
*/
@Override
protected void visitForNode(ForNode node) {
String varName = node.getVarName();
String localVar = varName + node.getId();
// Get CodeChunks for the initial/limit/increment values.
RangeArgs range = node.getRangeArgs();
CodeChunk.WithValue initial =
jsExprTranslator.translateToCodeChunk(
range.start(), templateTranslationContext, errorReporter);
CodeChunk.WithValue limit =
jsExprTranslator.translateToCodeChunk(
range.limit(), templateTranslationContext, errorReporter);
CodeChunk.WithValue increment =
jsExprTranslator.translateToCodeChunk(
range.increment(), templateTranslationContext, errorReporter);
// If the limit or increment are not raw integers, save them to a separate variable so that
// they are not calculated multiple times.
// No need to do so for initial, since it is only executed once.
if (!(range.limit().getRoot() instanceof IntegerNode)) {
limit = declare(localVar + "Limit", limit).ref();
}
if (!(range.increment().getRoot() instanceof IntegerNode)) {
increment = declare(localVar + "Increment", increment).ref();
}
// Populate Soy to JS var mappings with this for node's local variable.
templateTranslationContext.soyToJsVariableMappings().put(varName, id(localVar));
// Generate the CodeChunk for the loop body.
CodeChunk body = visitChildrenReturningCodeChunk(node);
// Create the entire for block.
CodeChunk forChunk = forLoop(localVar, initial, limit, increment, body);
jsCodeBuilder.append(forChunk);
}
/**
* Example:
* <pre>
* {call some.func data="all" /}
* {call some.func data="$boo.foo" /}
* {call some.func}
* {param goo: 88 /}
* {/call}
* {call some.func data="$boo"}
* {param goo}
* Hello {$name}
* {/param}
* {/call}
* </pre>
* might generate
* <pre>
* output += some.func(opt_data);
* output += some.func(opt_data.boo.foo);
* output += some.func({goo: 88});
* output += some.func(soy.$$assignDefaults({goo: 'Hello ' + opt_data.name}, opt_data.boo);
* </pre>
*/
@Override protected void visitCallNode(CallNode node) {
// If this node has any CallParamContentNode children those contents are not computable as JS
// expressions, visit them to generate code to define their respective 'param<n>' variables.
for (CallParamNode child : node.getChildren()) {
if (child instanceof CallParamContentNode && !isComputableAsJsExprsVisitor.exec(child)) {
visit(child);
}
}
// Add the call's result to the current output var.
CodeChunk.WithValue call =
genCallCodeUtils.gen(node, templateAliases, templateTranslationContext, errorReporter);
jsCodeBuilder.addChunkToOutputVar(call);
}
@Override protected void visitCallParamContentNode(CallParamContentNode node) {
// This node should only be visited when it's not computable as JS expressions, because this
// method just generates the code to define the temporary 'param<n>' variable.
if (isComputableAsJsExprsVisitor.exec(node)) {
throw new AssertionError(
"Should only define 'param<n>' when not computable as JS expressions.");
}
jsCodeBuilder.pushOutputVar("param" + node.getId());
visitChildren(node);
jsCodeBuilder.popOutputVar();
}
/**
* Example:
* <pre>
* {log}Blah {$boo}.{/log}
* </pre>
* might generate
* <pre>
* window.console.log('Blah ' + opt_data.boo + '.');
* </pre>
*
* <p> If the log msg is not computable as JS exprs, then it will be built in a local var
* logMsg_s##, e.g.
* <pre>
* var logMsg_s14 = ...
* window.console.log(logMsg_s14);
* </pre>
*/
@Override protected void visitLogNode(LogNode node) {
if (isComputableAsJsExprsVisitor.execOnChildren(node)) {
List<CodeChunk.WithValue> logMsgChunks = genJsExprsVisitor.execOnChildren(node);
jsCodeBuilder.append(WINDOW_CONSOLE_LOG.call(CodeChunkUtils.concatChunks(logMsgChunks)));
} else {
// Must build log msg in a local var logMsg_s##.
String outputVarName = "logMsg_s" + node.getId();
jsCodeBuilder.pushOutputVar(outputVarName);
visitChildren(node);
jsCodeBuilder.popOutputVar();
jsCodeBuilder.append(WINDOW_CONSOLE_LOG.call(id(outputVarName)));
}
}
/**
* Example:
* <pre>
* {debugger}
* </pre>
* generates
* <pre>
* debugger;
* </pre>
*/
@Override protected void visitDebuggerNode(DebuggerNode node) {
jsCodeBuilder.appendLine("debugger;");
}
// -----------------------------------------------------------------------------------------------
// Fallback implementation.
@Override protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
if (node instanceof BlockNode) {
visitChildren((BlockNode) node);
} else {
visitChildren((ParentSoyNode<?>) node);
}
return;
}
if (isComputableAsJsExprsVisitor.exec(node)) {
// Simply generate JS expressions for this node and add them to the current output var.
jsCodeBuilder.addChunksToOutputVar(genJsExprsVisitor.exec(node));
} else {
// Need to implement visit*Node() for the specific case.
throw new UnsupportedOperationException();
}
}
// -----------------------------------------------------------------------------------------------
// Helpers
/**
* Generate the JSDoc for the opt_data parameter.
*/
private String genParamsRecordType(TemplateNode node) {
Set<String> paramNames = new HashSet<>();
// Generate members for explicit params.
Map<String, String> record = new LinkedHashMap<>();
for (TemplateParam param : node.getParams()) {
JsType jsType = getJsType(param.type());
record.put(genParamAlias(param.name()), jsType.typeExprForRecordMember());
for (GoogRequire require : jsType.getGoogRequires()) {
jsCodeBuilder.addGoogRequire(require);
}
paramNames.add(param.name());
}
// Do the same for indirect params, if we can find them.
// If there's a conflict between the explicitly-declared type, and the type
// inferred from the indirect params, then the explicit type wins.
// Also note that indirect param types may not be inferrable if the target
// is not in the current compilation file set.
IndirectParamsInfo ipi = new FindIndirectParamsVisitor(templateRegistry).exec(node);
// If there are any calls outside of the file set, then we can't know
// the complete types of any indirect params. In such a case, we can simply
// omit the indirect params from the function type signature, since record
// types in JS allow additional undeclared fields to be present.
if (!ipi.mayHaveIndirectParamsInExternalCalls && !ipi.mayHaveIndirectParamsInExternalDelCalls) {
for (String indirectParamName : ipi.indirectParamTypes.keySet()) {
if (paramNames.contains(indirectParamName)) {
continue;
}
Collection<SoyType> paramTypes = ipi.indirectParamTypes.get(indirectParamName);
SoyType combinedType = typeOps.computeLowestCommonType(paramTypes);
// Note that Union folds duplicate types and flattens unions, so if
// the combinedType is already a union this will do the right thing.
// TODO: detect cases where nullable is not needed (requires flow
// analysis to determine if the template is always called.)
SoyType indirectParamType = typeOps.getTypeRegistry()
.getOrCreateUnionType(combinedType, NullType.getInstance());
JsType jsType = getJsType(indirectParamType);
// NOTE: we do not add goog.requires for indirect types. This is because it might introduce
// strict deps errors. This should be fine though since the transitive soy template that
// actually has the param will add them.
record.put(genParamAlias(indirectParamName), jsType.typeExprForRecordMember());
}
}
StringBuilder sb = new StringBuilder();
sb.append("{\n * ");
Joiner.on(",\n * ").withKeyValueSeparator(": ").appendTo(sb, record);
sb.append("\n * }");
return sb.toString();
}
/**
* Generate code to verify the runtime types of the input params. Also typecasts the
* input parameters and assigns them to local variables for use in the template.
* @param node the template node.
*/
protected void genParamTypeChecks(TemplateNode node) {
for (TemplateParam param : node.getAllParams()) {
if (param.declLoc() != TemplateParam.DeclLoc.HEADER) {
continue;
}
String paramName = param.name();
SoyType paramType = param.type();
CodeChunk.Generator generator = templateTranslationContext.codeGenerator();
CodeChunk.WithValue paramChunk =
TranslateExprNodeVisitor.genCodeForParamAccess(
paramName, param.isInjected());
JsType jsType = getJsType(paramType);
// The opt_param.name value that will be type-tested.
String paramAlias = genParamAlias(paramName);
CodeChunk.WithValue coerced = jsType.getValueCoercion(paramChunk, generator);
if (coerced != null) {
// since we have coercion logic, dump into a temporary
paramChunk = generator.declare(coerced).ref();
}
// The param value to assign
CodeChunk.WithValue value;
Optional<CodeChunk.WithValue> typeAssertion = jsType.getTypeAssertion(paramChunk, generator);
// The type-cast expression.
if (typeAssertion.isPresent()) {
value =
SOY_ASSERTS_ASSERT_TYPE.call(
typeAssertion.get(),
stringLiteral(paramName),
paramChunk,
stringLiteral(jsType.typeExpr()));
} else {
value = paramChunk;
}
String closureTypeExpr = jsSrcOptions.shouldGenerateJsdoc() ? jsType.typeExpr() : null;
jsCodeBuilder.append(declare(paramAlias, value, closureTypeExpr, jsType.getGoogRequires()));
templateTranslationContext
.soyToJsVariableMappings()
.put(paramName, id(paramAlias));
}
}
private JsType getJsType(SoyType paramType) {
boolean isIncrementalDom = !getClass().equals(GenJsCodeVisitor.class);
return JsType.forSoyType(paramType, isIncrementalDom);
}
/**
* Generate a name for the local variable which will store the value of a
* parameter, avoiding collision with JavaScript reserved words.
*/
private String genParamAlias(String paramName) {
return JsSrcUtils.isReservedWord(paramName) ? "param$" + paramName : paramName;
}
/**
* Return true if the template has at least one strict param.
*/
private boolean hasStrictParams(TemplateNode template) {
for (TemplateParam param : template.getParams()) {
if (param.declLoc() == TemplateParam.DeclLoc.HEADER) {
return true;
}
}
// Note: If there are only injected params, don't use strong typing for
// the function signature, because what it will produce is an empty struct.
return false;
}
}