/*
* Copyright 2015 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.pysrc.internal;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.template.soy.base.internal.LegacyInternalSyntaxException;
import com.google.template.soy.base.internal.SoyFileKind;
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.Operator;
import com.google.template.soy.internal.base.Pair;
import com.google.template.soy.internal.i18n.SoyBidiUtils;
import com.google.template.soy.pysrc.SoyPySrcOptions;
import com.google.template.soy.pysrc.internal.GenPyExprsVisitor.GenPyExprsVisitorFactory;
import com.google.template.soy.pysrc.internal.PyApiCallScopeBindingAnnotations.PyCurrentManifest;
import com.google.template.soy.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;
import com.google.template.soy.pysrc.restricted.PyFunctionExprBuilder;
import com.google.template.soy.shared.internal.FindCalleesNotInFileVisitor;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.CallBasicNode;
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.ForeachIfemptyNode;
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.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.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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.inject.Inject;
/**
* Visitor for generating full Python code (i.e. statements) for parse tree nodes.
*
* <p>{@link #gen} should be called on a full parse tree. Python 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 Python file (corresponding to one Soy file).
*
*/
final class GenPyCodeVisitor extends AbstractSoyNodeVisitor<List<String>> {
private static final SoyErrorKind NON_NAMESPACED_TEMPLATE =
SoyErrorKind.of("Called template does not reside in a namespace.");
/** The options configuration for this run. */
private final SoyPySrcOptions pySrcOptions;
/** The namespace manifest for all current and dependent sources. */
private final ImmutableMap<String, String> namespaceManifest;
@VisibleForTesting protected PyCodeBuilder pyCodeBuilder;
private final IsComputableAsPyExprVisitor isComputableAsPyExprVisitor;
private final GenPyExprsVisitorFactory genPyExprsVisitorFactory;
@VisibleForTesting protected GenPyExprsVisitor genPyExprsVisitor;
private final GenPyCallExprVisitor genPyCallExprVisitor;
/** @see LocalVariableStack */
@VisibleForTesting protected LocalVariableStack localVarExprs;
@Inject
GenPyCodeVisitor(
SoyPySrcOptions pySrcOptions,
@PyCurrentManifest ImmutableMap<String, String> currentManifest,
IsComputableAsPyExprVisitor isComputableAsPyExprVisitor,
GenPyExprsVisitorFactory genPyExprsVisitorFactory,
GenPyCallExprVisitor genPyCallExprVisitor) {
this.pySrcOptions = pySrcOptions;
this.isComputableAsPyExprVisitor = isComputableAsPyExprVisitor;
this.genPyExprsVisitorFactory = genPyExprsVisitorFactory;
this.genPyCallExprVisitor = genPyCallExprVisitor;
this.namespaceManifest =
new ImmutableMap.Builder<String, String>()
.putAll(pySrcOptions.getNamespaceManifest())
.putAll(currentManifest)
.build();
}
public List<String> gen(SoyFileSetNode node, ErrorReporter errorReporter) {
// All these fields should move into Impl but are currently exposed for tests.
pyCodeBuilder = null;
genPyExprsVisitor = null;
localVarExprs = null;
return new Impl(errorReporter).exec(node);
}
@VisibleForTesting
void visitForTesting(SoyNode node, ErrorReporter errorReporter) {
new Impl(errorReporter).exec(node);
}
private final class Impl extends AbstractSoyNodeVisitor<List<String>> {
/** The contents of the generated Python files. */
private List<String> pyFilesContents;
final ErrorReporter errorReporter;
Impl(ErrorReporter reporter) {
this.errorReporter = reporter;
}
@Override
public List<String> exec(SoyNode node) {
pyFilesContents = new ArrayList<>();
visit(node);
return pyFilesContents;
}
/**
* Visit all the children of a provided node and combine the results into one expression where
* possible. This will let us avoid some {@code output.append} calls and save a bit of time.
*/
@Override
protected void visitChildren(ParentSoyNode<?> node) {
// If the first child cannot be written as an expression, we need to init the output variable
// first or face potential scoping issues with the output variable being initialized too late.
if (node.numChildren() > 0 && !isComputableAsPyExprVisitor.exec(node.getChild(0))) {
pyCodeBuilder.initOutputVarIfNecessary();
}
List<PyExpr> childPyExprs = new ArrayList<>();
for (SoyNode child : node.getChildren()) {
if (isComputableAsPyExprVisitor.exec(child)) {
childPyExprs.addAll(genPyExprsVisitor.exec(child));
} else {
// We've reached a child that is not computable as a Python expression.
// First add the PyExprs from preceding consecutive siblings that are computable as Python
// expressions (if any).
if (!childPyExprs.isEmpty()) {
pyCodeBuilder.addToOutputVar(childPyExprs);
childPyExprs.clear();
}
// Now append the code for this child.
visit(child);
}
}
// Add the PyExprs from the last few children (if any).
if (!childPyExprs.isEmpty()) {
pyCodeBuilder.addToOutputVar(childPyExprs);
childPyExprs.clear();
}
}
// ---------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override
protected void visitSoyFileSetNode(SoyFileSetNode node) {
for (SoyFileNode soyFile : node.getChildren()) {
visit(soyFile);
}
}
/**
* Visit a SoyFileNode and generate it's Python output.
*
* <p>This visitor generates the necessary imports and configuration needed for all Python
* output files. This includes imports of runtime libraries, external templates called from
* within this file, and namespacing configuration.
*
* <p>Template generation is deferred to other visitors.
*
* <p>Example Output:
*
* <pre>
* # coding=utf-8
* """ This file was automatically generated from my-templates.soy.
* Please don't edit this file by hand.
* """
*
* ...
* </pre>
*/
@Override
protected void visitSoyFileNode(SoyFileNode node) {
if (node.getSoyFileKind() != SoyFileKind.SRC) {
return; // don't generate code for deps
}
pyCodeBuilder = new PyCodeBuilder();
// Encode all source files in utf-8 to allow for special unicode characters in the generated
// literals.
pyCodeBuilder.appendLine("# coding=utf-8");
pyCodeBuilder.appendLine(
"\"\"\" This file was automatically generated from ", node.getFileName(), ".");
pyCodeBuilder.appendLine("Please don't edit this file by hand.");
pyCodeBuilder.appendLine();
pyCodeBuilder.appendLine("SOY_NAMESPACE: '" + node.getNamespace() + "'.");
// Output a section containing optionally-parsed compiler directives in comments.
pyCodeBuilder.appendLine();
if (node.getNamespace() != null) {
pyCodeBuilder.appendLine("Templates in namespace ", node.getNamespace(), ".");
}
pyCodeBuilder.appendLine("\"\"\"");
// Add code to define Python namespaces and add import calls for libraries.
pyCodeBuilder.appendLine();
addCodeToRequireGeneralDeps();
addCodeToRequireSoyNamespaces(node);
addCodeToFixUnicodeStrings();
if (SoyTreeUtils.hasNodesOfType(node, DebuggerNode.class)) {
pyCodeBuilder.appendLine("import pdb");
}
// Add code for each template.
for (TemplateNode template : node.getChildren()) {
pyCodeBuilder.appendLine().appendLine();
visit(template);
}
pyFilesContents.add(pyCodeBuilder.getCode());
pyCodeBuilder = null;
}
/**
* Visit a TemplateNode and generate a corresponding function.
*
* <p>Example:
*
* <pre>
* def myfunc(data, ijData):
* output = ''
* ...
* ...
* return output
* </pre>
*/
@Override
protected void visitTemplateNode(TemplateNode node) {
localVarExprs = new LocalVariableStack();
genPyExprsVisitor = genPyExprsVisitorFactory.create(localVarExprs, errorReporter);
// Generate function definition up to colon.
pyCodeBuilder.appendLine(
"def ",
node.getPartialTemplateName().substring(1),
// These defaults are safe because soy only ever reads from these parameters. If that
// changes, bad things could happen.
"(data={}, ijData={}):");
pyCodeBuilder.increaseIndent();
generateFunctionBody(node);
// Dedent to end the function.
pyCodeBuilder.decreaseIndent();
}
/**
* Visit a TemplateDelegateNode and generate the corresponding function along with the delegate
* registration.
*
* <p>Example:
*
* <pre>
* def myfunc(data=None, ijData=None):
* ...
* runtime.register_delegate_fn('delname', 'delvariant', 0, myfunc, 'myfunc')
* </pre>
*/
@Override
protected void visitTemplateDelegateNode(TemplateDelegateNode node) {
// Generate the template first, before registering the delegate function.
visitTemplateNode(node);
// Register the function as a delegate function.
String delTemplateIdExprText = "'" + node.getDelTemplateName() + "'";
String delTemplateVariantExprText = "'" + node.getDelTemplateVariant() + "'";
pyCodeBuilder.appendLine(
"runtime.register_delegate_fn(",
delTemplateIdExprText,
", ",
delTemplateVariantExprText,
", ",
node.getDelPriority().toString(),
", ",
node.getPartialTemplateName().substring(1),
", '",
node.getPartialTemplateName().substring(1),
"')");
}
@Override
protected void visitPrintNode(PrintNode node) {
pyCodeBuilder.addToOutputVar(genPyExprsVisitor.exec(node));
}
/**
* Visit an IfNode and generate a full conditional statement, or an inline ternary conditional
* expression if all the children are computable as expressions.
*
* <p>Example:
*
* <pre>
* {if $boo > 0}
* ...
* {/if}
* </pre>
*
* might generate
*
* <pre>
* if data.get('boo') > 0:
* ...
* </pre>
*/
@Override
protected void visitIfNode(IfNode node) {
if (isComputableAsPyExprVisitor.exec(node)) {
pyCodeBuilder.addToOutputVar(genPyExprsVisitor.exec(node));
return;
}
// Not computable as Python expressions, so generate full code.
TranslateToPyExprVisitor translator =
new TranslateToPyExprVisitor(localVarExprs, errorReporter);
for (SoyNode child : node.getChildren()) {
if (child instanceof IfCondNode) {
IfCondNode icn = (IfCondNode) child;
PyExpr condPyExpr = translator.exec(icn.getExpr());
if (icn.getCommandName().equals("if")) {
pyCodeBuilder.appendLine("if ", condPyExpr.getText(), ":");
} else {
pyCodeBuilder.appendLine("elif ", condPyExpr.getText(), ":");
}
pyCodeBuilder.increaseIndent();
visitChildren(icn);
pyCodeBuilder.decreaseIndent();
} else if (child instanceof IfElseNode) {
pyCodeBuilder.appendLine("else:");
pyCodeBuilder.increaseIndent();
visitChildren((IfElseNode) child);
pyCodeBuilder.decreaseIndent();
} else {
throw new AssertionError("Unexpected if child node type. Child: " + child);
}
}
}
/**
* Python does not support switch statements, so just replace with if: ... elif: ... else: ...
* As some expressions may generate different results each time, the expression is stored before
* conditionals (which prevents expression inlining).
*
* <p>Example:
*
* <pre>
* {switch $boo}
* {case 0}
* ...
* {case 1, 2}
* ...
* {default}
* ...
* {/switch}
* </pre>
*
* might generate
*
* <pre>
* switchValue = data.get('boo')
* if switchValue == 0:
* ...
* elif switchValue == 1:
* ...
* elif switchValue == 2:
* ...
* else:
* ...
* </pre>
*/
@Override
protected void visitSwitchNode(SwitchNode node) {
// Run the switch value creation first to ensure side effects always occur.
TranslateToPyExprVisitor translator =
new TranslateToPyExprVisitor(localVarExprs, errorReporter);
String switchValueVarName = "switchValue";
PyExpr switchValuePyExpr = translator.exec(node.getExpr());
pyCodeBuilder.appendLine(switchValueVarName, " = ", switchValuePyExpr.getText());
// If a Switch with only a default is provided (no case statements), just execute the inner
// code directly.
if (node.getChildren().size() == 1 && node.getChild(0) instanceof SwitchDefaultNode) {
visitChildren((SwitchDefaultNode) node.getChild(0));
return;
}
boolean isFirstCase = true;
for (SoyNode child : node.getChildren()) {
if (child instanceof SwitchCaseNode) {
SwitchCaseNode scn = (SwitchCaseNode) child;
for (ExprNode caseExpr : scn.getExprList()) {
PyExpr casePyExpr = translator.exec(caseExpr);
PyExpr conditionFn =
new PyFunctionExprBuilder("runtime.type_safe_eq")
.addArg(new PyExpr(switchValueVarName, Integer.MAX_VALUE))
.addArg(casePyExpr)
.asPyExpr();
if (isFirstCase) {
pyCodeBuilder.appendLineStart("if ").append(conditionFn.getText()).appendLineEnd(":");
isFirstCase = false;
} else {
pyCodeBuilder
.appendLineStart("elif ")
.append(conditionFn.getText())
.appendLineEnd(":");
}
pyCodeBuilder.increaseIndent();
visitChildren(scn);
pyCodeBuilder.decreaseIndent();
}
} else if (child instanceof SwitchDefaultNode) {
SwitchDefaultNode sdn = (SwitchDefaultNode) child;
pyCodeBuilder.appendLine("else:");
pyCodeBuilder.increaseIndent();
visitChildren(sdn);
pyCodeBuilder.decreaseIndent();
} else {
throw new AssertionError("Unexpected switch child node type. Child: " + child);
}
}
}
/**
* Visits a ForNode and generates a for loop over a given range.
*
* <p>Example:
*
* <pre>
* {for $i in range(1, $boo)}
* ...
* {/for}
* </pre>
*
* might generate
*
* <pre>
* for i4 in xrange(1, data.get('boo')):
* ...
* </pre>
*/
@Override
protected void visitForNode(ForNode node) {
TranslateToPyExprVisitor translator =
new TranslateToPyExprVisitor(localVarExprs, errorReporter);
String varName = node.getVarName();
String nodeId = Integer.toString(node.getId());
// The start of the Python 'for' loop.
pyCodeBuilder.appendLineStart("for ", varName, nodeId, " in ");
// Build the xrange call. Since the Python param syntax matches Soy range syntax, params can
// be directly dropped in.
PyFunctionExprBuilder funcBuilder = new PyFunctionExprBuilder("xrange");
for (ExprRootNode arg : node.getExprList()) {
funcBuilder.addArg(translator.exec(arg));
}
pyCodeBuilder.appendLineEnd(funcBuilder.asPyExpr().getText(), ":");
// Add a new localVarExprs frame and populate it with the translations from this node.
localVarExprs.pushFrame();
localVarExprs.addVariable(varName, new PyExpr(varName + nodeId, Integer.MAX_VALUE));
// Generate the code for the loop body.
pyCodeBuilder.increaseIndent();
visitChildren(node);
pyCodeBuilder.decreaseIndent();
// Remove the localVarTranslations frame that we added above.
localVarExprs.popFrame();
}
/**
* The top level ForeachNode primarily serves to test for the ifempty case. If present, the loop
* is wrapped in an if statement which checks for data in the list before iterating.
*
* <p>Example:
*
* <pre>
* {foreach $foo in $boo}
* ...
* {ifempty}
* ...
* {/foreach}
* </pre>
*
* might generate
*
* <pre>
* fooList2 = data.get('boo')
* if fooList2:
* ...loop...
* else:
* ...
* </pre>
*/
@Override
protected void visitForeachNode(ForeachNode node) {
// Build the local variable names.
ForeachNonemptyNode nonEmptyNode = (ForeachNonemptyNode) node.getChild(0);
String baseVarName = nonEmptyNode.getVarName();
String listVarName = String.format("%sList%d", baseVarName, node.getId());
// Define list variable
TranslateToPyExprVisitor translator =
new TranslateToPyExprVisitor(localVarExprs, errorReporter);
PyExpr dataRefPyExpr = translator.exec(node.getExpr());
pyCodeBuilder.appendLine(listVarName, " = ", dataRefPyExpr.getText());
// If has 'ifempty' node, add the wrapper 'if' statement.
boolean hasIfemptyNode = node.numChildren() == 2;
if (hasIfemptyNode) {
// Empty lists are falsy in Python.
pyCodeBuilder.appendLine("if ", listVarName, ":");
pyCodeBuilder.increaseIndent();
}
// Generate code for nonempty case.
visit(nonEmptyNode);
// If has 'ifempty' node, add the 'else' block of the wrapper 'if' statement.
if (hasIfemptyNode) {
pyCodeBuilder.decreaseIndent();
pyCodeBuilder.appendLine("else:");
pyCodeBuilder.increaseIndent();
// Generate code for empty case.
visit(node.getChild(1));
pyCodeBuilder.decreaseIndent();
}
}
/**
* The ForeachNonemptyNode performs the actual looping. We use a standard {@code for} loop,
* except that instead of looping directly over the list, we loop over an enumeration to have
* easy access to the index along with the data.
*
* <p>Example:
*
* <pre>
* {foreach $foo in $boo}
* ...
* {/foreach}
* </pre>
*
* might generate
*
* <pre>
* fooList2 = data.get('boo')
* for fooIndex2, fooData2 in enumerate(fooList2):
* ...
* </pre>
*/
@Override
protected void visitForeachNonemptyNode(ForeachNonemptyNode node) {
// Build the local variable names.
String baseVarName = node.getVarName();
String foreachNodeId = Integer.toString(node.getForeachNodeId());
String listVarName = baseVarName + "List" + foreachNodeId;
String indexVarName = baseVarName + "Index" + foreachNodeId;
String dataVarName = baseVarName + "Data" + foreachNodeId;
// Create the loop with an enumeration.
pyCodeBuilder.appendLine(
"for ", indexVarName, ", ", dataVarName, " in enumerate(", listVarName, "):");
pyCodeBuilder.increaseIndent();
// Add a new localVarExprs frame and populate it with the translations from this loop.
int eqPrecedence = PyExprUtils.pyPrecedenceForOperator(Operator.EQUAL);
localVarExprs.pushFrame();
localVarExprs
.addVariable(baseVarName, new PyExpr(dataVarName, Integer.MAX_VALUE))
.addVariable(baseVarName + "__isFirst", new PyExpr(indexVarName + " == 0", eqPrecedence))
.addVariable(
baseVarName + "__isLast",
new PyExpr(indexVarName + " == len(" + listVarName + ") - 1", eqPrecedence))
.addVariable(baseVarName + "__index", new PyExpr(indexVarName, Integer.MAX_VALUE));
// Generate the code for the loop body.
visitChildren(node);
// Remove the localVarExprs frame that we added above.
localVarExprs.popFrame();
// The end of the Python 'for' loop.
pyCodeBuilder.decreaseIndent();
}
@Override
protected void visitForeachIfemptyNode(ForeachIfemptyNode node) {
visitChildren(node);
}
/**
* Visits a let node which accepts a value and stores it as a unique variable. The unique
* variable name is stored in the LocalVariableStack for use by any subsequent code.
*
* <p>Example:
*
* <pre>
* {let $boo: $foo[$moo] /}
* </pre>
*
* might generate
*
* <pre>
* boo3 = data.get('foo')['moo']
* </pre>
*/
@Override
protected void visitLetValueNode(LetValueNode node) {
String generatedVarName = node.getUniqueVarName();
// Generate code to define the local var.
TranslateToPyExprVisitor translator =
new TranslateToPyExprVisitor(localVarExprs, errorReporter);
PyExpr valuePyExpr = translator.exec(node.getValueExpr());
pyCodeBuilder.appendLine(generatedVarName, " = ", valuePyExpr.getText());
// Add a mapping for generating future references to this local var.
localVarExprs.addVariable(node.getVarName(), new PyExpr(generatedVarName, Integer.MAX_VALUE));
}
/**
* Visits a let node which contains a content section and stores it as a unique variable. The
* unique variable name is stored in the LocalVariableStack for use by any subsequent code.
*
* <p>Note, this is one of the location where Strict mode is enforced in Python templates. As
* such, all LetContentNodes must have a contentKind specified.
*
* <p>Example:
*
* <pre>
* {let $boo kind="html"}
* Hello {$name}
* {/let}
* </pre>
*
* might generate
*
* <pre>
* boo3 = sanitize.SanitizedHtml(''.join(['Hello ', sanitize.escape_html(data.get('name'))])
* </pre>
*/
@Override
protected void visitLetContentNode(LetContentNode node) {
if (node.getContentKind() == null) {
throw LegacyInternalSyntaxException.createWithMetaInfo(
"Let content node is missing a content kind. This may be due to using a non-strict "
+ "template, which is unsupported in the Python compiler.",
node.getSourceLocation());
}
String generatedVarName = node.getUniqueVarName();
// Traverse the children and push them onto the generated variable.
localVarExprs.pushFrame();
pyCodeBuilder.pushOutputVar(generatedVarName);
visitChildren(node);
PyExpr generatedContent = pyCodeBuilder.getOutputAsString();
pyCodeBuilder.popOutputVar();
localVarExprs.popFrame();
// Mark the result as being escaped to the appropriate kind (e.g., "sanitize.SanitizedHtml").
pyCodeBuilder.appendLine(
generatedVarName,
" = ",
PyExprUtils.wrapAsSanitizedContent(node.getContentKind(), generatedContent).getText());
// Add a mapping for generating future references to this local var.
localVarExprs.addVariable(node.getVarName(), new PyExpr(generatedVarName, Integer.MAX_VALUE));
}
/**
* Visits a call node and generates the syntax needed to call another template. If all of the
* children can be represented as expressions, this is built as an expression itself. If not,
* the non-expression params are saved as {@code param<n>} variables before the function call.
*/
@Override
protected void visitCallNode(CallNode node) {
// If this node has any param children whose contents are not computable as Python
// expressions, visit them to generate code to define their respective 'param<n>' variables.
for (CallParamNode child : node.getChildren()) {
if (child instanceof CallParamContentNode && !isComputableAsPyExprVisitor.exec(child)) {
visit(child);
}
}
pyCodeBuilder.addToOutputVar(
genPyCallExprVisitor.exec(node, localVarExprs, errorReporter).toPyString());
}
/**
* Visits a call param content node which isn't computable as a PyExpr and stores its content in
* a variable with the name {@code param<n>} where n is the node's id.
*/
@Override
protected void visitCallParamContentNode(CallParamContentNode node) {
// This node should only be visited when it's not computable as Python expressions.
Preconditions.checkArgument(
!isComputableAsPyExprVisitor.exec(node),
"Should only define 'param<n>' when not computable as Python expressions.");
pyCodeBuilder.pushOutputVar("param" + node.getId());
pyCodeBuilder.initOutputVarIfNecessary();
visitChildren(node);
pyCodeBuilder.popOutputVar();
}
@Override
protected void visitDebuggerNode(DebuggerNode node) {
pyCodeBuilder.appendLine("pdb.set_trace()");
}
@Override
protected void visitLogNode(LogNode node) {
String outputVarName = "logger_" + node.getId();
pyCodeBuilder.pushOutputVar(outputVarName);
pyCodeBuilder.initOutputVarIfNecessary();
visitChildren(node);
pyCodeBuilder.popOutputVar();
pyCodeBuilder.appendLine("print " + outputVarName);
}
// ---------------------------------------------------------------------------------------------
// Fallback implementation.
@Override
protected void visitSoyNode(SoyNode node) {
if (isComputableAsPyExprVisitor.exec(node)) {
// Generate Python expressions for this node and add them to the current output var.
pyCodeBuilder.addToOutputVar(genPyExprsVisitor.exec(node));
} else {
// Need to implement visit*Node() for the specific case.
throw new UnsupportedOperationException();
}
}
// ---------------------------------------------------------------------------------------------
// Utility methods.
/** Helper for visitSoyFileNode(SoyFileNode) to add code to require general dependencies. */
private void addCodeToRequireGeneralDeps() {
pyCodeBuilder.appendLine("from __future__ import unicode_literals");
pyCodeBuilder.appendLine("import collections");
pyCodeBuilder.appendLine("import math");
pyCodeBuilder.appendLine("import random");
pyCodeBuilder.appendLine("import sys");
// TODO(dcphillips): limit this based on usage?
pyCodeBuilder.appendLine("from ", pySrcOptions.getRuntimePath(), " import bidi");
pyCodeBuilder.appendLine("from ", pySrcOptions.getRuntimePath(), " import directives");
pyCodeBuilder.appendLine("from ", pySrcOptions.getRuntimePath(), " import runtime");
pyCodeBuilder.appendLine("from ", pySrcOptions.getRuntimePath(), " import sanitize");
pyCodeBuilder.appendLine();
if (!pySrcOptions.getBidiIsRtlFn().isEmpty()) {
int dotIndex = pySrcOptions.getBidiIsRtlFn().lastIndexOf('.');
// When importing the module, we'll use the constant name to avoid potential conflicts.
String bidiModulePath = pySrcOptions.getBidiIsRtlFn().substring(0, dotIndex);
Pair<String, String> nameSpaceAndName = namespaceAndNameFromModule(bidiModulePath);
String bidiNamespace = nameSpaceAndName.first;
String bidiModuleName = nameSpaceAndName.second;
pyCodeBuilder.appendLine(
"from ",
bidiNamespace,
" import ",
bidiModuleName,
" as ",
SoyBidiUtils.IS_RTL_MODULE_ALIAS);
}
// Add import and instantiate statements for translator module
// TODO(steveyang): remember the check when implementing MsgNode
if (!pySrcOptions.getTranslationClass().isEmpty()) {
Pair<String, String> nameSpaceAndName =
namespaceAndNameFromModule(pySrcOptions.getTranslationClass());
String translationNamespace = nameSpaceAndName.first;
String translationName = nameSpaceAndName.second;
pyCodeBuilder.appendLine("from ", translationNamespace, " import ", translationName);
pyCodeBuilder.appendLine(PyExprUtils.TRANSLATOR_NAME, " = ", translationName, "()");
}
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to require Soy namespaces.
*
* @param soyFile The node we're visiting.
*/
private void addCodeToRequireSoyNamespaces(SoyFileNode soyFile) {
SortedSet<String> calleeModules = new TreeSet<>();
for (CallBasicNode node : new FindCalleesNotInFileVisitor().exec(soyFile)) {
String calleeNotInFile = node.getCalleeName();
int lastDotIndex = calleeNotInFile.lastIndexOf('.');
if (lastDotIndex == -1) {
errorReporter.report(node.getSourceLocation(), NON_NAMESPACED_TEMPLATE);
continue;
}
String calleeModule = calleeNotInFile.substring(0, lastDotIndex);
if (!calleeModule.isEmpty()) {
calleeModules.add(calleeModule);
}
}
for (String calleeModule : calleeModules) {
Pair<String, String> nameSpaceAndName = namespaceAndNameFromModule(calleeModule);
String calleeNamespace = nameSpaceAndName.first;
String calleeName = nameSpaceAndName.second;
if (namespaceManifest.containsKey(calleeModule)) {
pyCodeBuilder.appendLine(
"import ", namespaceManifest.get(calleeModule), " as ", calleeName);
} else {
pyCodeBuilder.appendLineStart(
calleeName,
" = runtime.namespaced_import('",
calleeName,
"', namespace='",
calleeNamespace,
"'");
if (!pySrcOptions.getEnvironmentModulePath().isEmpty()) {
pyCodeBuilder
.append(", environment_path='")
.append(pySrcOptions.getEnvironmentModulePath(), "'");
}
pyCodeBuilder.appendLineEnd(")");
}
}
// Store the entire manifest for use at runtime.
pyCodeBuilder.appendLine("NAMESPACE_MANIFEST = {");
pyCodeBuilder.increaseIndentTwice();
for (Map.Entry<String, String> entry : namespaceManifest.entrySet()) {
pyCodeBuilder.appendLine("'", entry.getKey(), "': '", entry.getValue(), "',");
}
pyCodeBuilder.decreaseIndentTwice();
pyCodeBuilder.appendLine("}");
pyCodeBuilder.appendLine();
}
/**
* Helper for visitSoyFileNode(SoyFileNode) to add code to turn byte strings into unicode
* strings for Python 2.
*/
private void addCodeToFixUnicodeStrings() {
pyCodeBuilder.appendLine("try:");
pyCodeBuilder.increaseIndent();
pyCodeBuilder.appendLine("str = unicode");
pyCodeBuilder.decreaseIndent();
pyCodeBuilder.appendLine("except NameError:");
pyCodeBuilder.increaseIndent();
pyCodeBuilder.appendLine("pass");
pyCodeBuilder.decreaseIndent();
pyCodeBuilder.appendLine();
}
/** Helper for visitTemplateNode which generates the function body. */
private void generateFunctionBody(TemplateNode node) {
// Add a new frame for local variable translations.
localVarExprs.pushFrame();
pyCodeBuilder.pushOutputVar("output");
visitChildren(node);
PyExpr resultPyExpr = pyCodeBuilder.getOutputAsString();
pyCodeBuilder.popOutputVar();
// 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.
resultPyExpr = PyExprUtils.wrapAsSanitizedContent(node.getContentKind(), resultPyExpr);
pyCodeBuilder.appendLine("return ", resultPyExpr.getText());
localVarExprs.popFrame();
}
}
/**
* Helper to retrieve the namespace and name from a module name.
*
* @param moduleName Python module name in dot notation format.
*/
private static Pair<String, String> namespaceAndNameFromModule(String moduleName) {
String namespace = moduleName;
String name = moduleName;
int lastDotIndex = moduleName.lastIndexOf('.');
if (lastDotIndex != -1) {
namespace = moduleName.substring(0, lastDotIndex);
name = moduleName.substring(lastDotIndex + 1);
}
return Pair.of(namespace, name);
}
}