diff --git a/java/src/com/google/template/soy/pysrc/internal/GenPyCodeVisitor.java b/java/src/com/google/template/soy/pysrc/internal/GenPyCodeVisitor.java index 52c13cd42a..1d5aa1e8dd 100644 --- a/java/src/com/google/template/soy/pysrc/internal/GenPyCodeVisitor.java +++ b/java/src/com/google/template/soy/pysrc/internal/GenPyCodeVisitor.java @@ -22,6 +22,7 @@ import com.google.template.soy.internal.base.Pair; import com.google.template.soy.internal.i18n.SoyBidiUtils; import com.google.template.soy.pysrc.internal.GenPyExprsVisitor.GenPyExprsVisitorFactory; +import com.google.template.soy.pysrc.internal.TranslateToPyExprVisitor.TranslateToPyExprVisitorFactory; import com.google.template.soy.pysrc.restricted.PyExpr; import com.google.template.soy.pysrc.restricted.PyExprUtils; import com.google.template.soy.shared.internal.FindCalleesNotInFileVisitor; @@ -30,6 +31,9 @@ import com.google.template.soy.shared.restricted.ApiCallScopeBindingAnnotations.TranslationPyModuleName; import com.google.template.soy.sharedpasses.ShouldEnsureDataIsDefinedVisitor; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; +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.PrintNode; import com.google.template.soy.soytree.SoyFileNode; import com.google.template.soy.soytree.SoyFileSetNode; @@ -75,10 +79,10 @@ final class GenPyCodeVisitor extends AbstractSoyNodeVisitor> { @VisibleForTesting protected GenPyExprsVisitor genPyExprsVisitor; + private final TranslateToPyExprVisitorFactory translateToPyExprVisitorFactory; + /** * @param runtimePath The module path for the runtime libraries. - * @param isComputableAsPyExprVisitor The IsComputableAsPyExprVisitor to use. - * @param genPyExprsVisitorFactory Factory for creating an instance of GenPyExprsVisitor. * @param translationPyModuleName Python module name used in python runtime to instantiate * translation. */ @@ -87,12 +91,14 @@ final class GenPyCodeVisitor extends AbstractSoyNodeVisitor> { @BidiIsRtlFn String bidiIsRtlFn, @TranslationPyModuleName String translationPyModuleName, IsComputableAsPyExprVisitor isComputableAsPyExprVisitor, - GenPyExprsVisitorFactory genPyExprsVisitorFactory) { + GenPyExprsVisitorFactory genPyExprsVisitorFactory, + TranslateToPyExprVisitorFactory translateToPyExprVisitorFactory) { this.runtimePath = runtimePath; this.bidiIsRtlFn = bidiIsRtlFn; this.translationPyModuleName = translationPyModuleName; this.isComputableAsPyExprVisitor = isComputableAsPyExprVisitor; this.genPyExprsVisitorFactory = genPyExprsVisitorFactory; + this.translateToPyExprVisitorFactory = translateToPyExprVisitorFactory; } @@ -246,6 +252,56 @@ final class GenPyCodeVisitor extends AbstractSoyNodeVisitor> { 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. + * + *

Example: + *

+   *   {if $boo > 0}
+   *     ...
+   *   {/if}
+   * 
+ * might generate + *
+   *   if opt_data.get('boo') > 0:
+   *     ...
+   * 
+ */ + @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 = translateToPyExprVisitorFactory.create(); + for (SoyNode child : node.getChildren()) { + if (child instanceof IfCondNode) { + IfCondNode icn = (IfCondNode) child; + PyExpr condPyExpr = translator.exec(icn.getExprUnion().getExpr()); + + if (icn.getCommandName().equals("if")) { + pyCodeBuilder.appendLine("if ", condPyExpr.getText(), ":"); + } else { + pyCodeBuilder.appendLine("elif ", condPyExpr.getText(), ":"); + } + + pyCodeBuilder.increaseIndent(); + visit(icn); + pyCodeBuilder.decreaseIndent(); + + } else if (child instanceof IfElseNode) { + pyCodeBuilder.appendLine("else:"); + pyCodeBuilder.increaseIndent(); + visit(child); + pyCodeBuilder.decreaseIndent(); + } else { + throw new AssertionError("Unexpected if child node type. Child: " + child); + } + } + } + // ----------------------------------------------------------------------------------------------- // Fallback implementation. diff --git a/java/src/com/google/template/soy/pysrc/internal/GenPyExprsVisitor.java b/java/src/com/google/template/soy/pysrc/internal/GenPyExprsVisitor.java index 37158c20ce..0dc3d15bc7 100644 --- a/java/src/com/google/template/soy/pysrc/internal/GenPyExprsVisitor.java +++ b/java/src/com/google/template/soy/pysrc/internal/GenPyExprsVisitor.java @@ -29,6 +29,9 @@ import com.google.template.soy.pysrc.restricted.PyStringExpr; import com.google.template.soy.pysrc.restricted.SoyPySrcPrintDirective; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; +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.MsgFallbackGroupNode; import com.google.template.soy.soytree.MsgNode; import com.google.template.soy.soytree.PrintDirectiveNode; @@ -207,4 +210,62 @@ public static interface GenPyExprsVisitorFactory { MsgFuncGenerator msgFuncGenerator = msgFuncGeneratorFactory.create(node); pyExprs.add(msgFuncGenerator.getPyExpr()); } + + /** + * If all the children are computable as expressions, the IfNode can be written as a ternary + * conditional expression. + */ + @Override protected void visitIfNode(IfNode node) { + // Create another instance of this visitor for generating Python expressions from children. + GenPyExprsVisitor genPyExprsVisitor = genPyExprsVisitorFactory.create(); + TranslateToPyExprVisitor translator = translateToPyExprVisitorFactory.create(); + + StringBuilder pyExprTextSb = new StringBuilder(); + + boolean hasElse = false; + for (SoyNode child : node.getChildren()) { + + if (child instanceof IfCondNode) { + IfCondNode icn = (IfCondNode) child; + + // Python ternary conditional expressions modify the order of the conditional from + // ? : to + // if else + PyExpr condBlock = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(icn)).toPyString(); + condBlock = PyExprUtils.maybeProtect(condBlock, + PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL)); + pyExprTextSb.append(condBlock.getText()); + + // Append the conditional and if/else syntax. + PyExpr condPyExpr = translator.exec(icn.getExprUnion().getExpr()); + pyExprTextSb.append(" if ").append(condPyExpr.getText()).append(" else "); + + } else if (child instanceof IfElseNode) { + hasElse = true; + IfElseNode ien = (IfElseNode) child; + + PyExpr elseBlock = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(ien)).toPyString(); + pyExprTextSb.append(elseBlock.getText()); + } else { + throw new AssertionError("Unexpected if child node type. Child: " + child); + } + } + + if (!hasElse) { + pyExprTextSb.append("''"); + } + + // By their nature, inline'd conditionals can only contain output strings, so they can be + // treated as a string type with a conditional precedence. + pyExprs.add(new PyStringExpr(pyExprTextSb.toString(), + PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL))); + } + + @Override protected void visitIfCondNode(IfCondNode node) { + visitChildren(node); + } + + @Override protected void visitIfElseNode(IfElseNode node) { + visitChildren(node); + } } diff --git a/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java b/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java index c05c356d56..86386d51cd 100644 --- a/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java +++ b/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java @@ -216,7 +216,7 @@ private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyP default: { PyExpr value = visit(node); - return genMaybeProtect(value, Integer.MAX_VALUE); + return PyExprUtils.maybeProtect(value, Integer.MAX_VALUE).getText(); } } } @@ -260,6 +260,7 @@ private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyP @Override protected PyExpr visitConditionalOpNode(ConditionalOpNode node) { // Retrieve the operands. Operator op = Operator.CONDITIONAL; + int conditionalPrecedence = PyExprUtils.pyPrecedenceForOperator(op); List syntax = op.getSyntax(); List operandExprs = visitChildren(node); @@ -273,13 +274,13 @@ private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyP // Python's ternary operator switches the order from ? : to // if else . StringBuilder exprSb = new StringBuilder(); - exprSb.append(genMaybeProtect(trueExpr, PyExprUtils.pyPrecedenceForOperator(op))); + exprSb.append(PyExprUtils.maybeProtect(trueExpr, conditionalPrecedence).getText()); exprSb.append(" if "); - exprSb.append(genMaybeProtect(conditionalExpr, PyExprUtils.pyPrecedenceForOperator(op))); + exprSb.append(PyExprUtils.maybeProtect(conditionalExpr, conditionalPrecedence).getText()); exprSb.append(" else "); - exprSb.append(genMaybeProtect(falseExpr, PyExprUtils.pyPrecedenceForOperator(op))); + exprSb.append(PyExprUtils.maybeProtect(falseExpr, conditionalPrecedence).getText()); - return new PyExpr(exprSb.toString(), PyExprUtils.pyPrecedenceForOperator(op)); + return new PyExpr(exprSb.toString(), conditionalPrecedence); } @Override protected PyExpr visitFunctionNode(FunctionNode node) { @@ -367,8 +368,4 @@ private PyExpr genPyExprUsingSoySyntax(OperatorNode opNode) { return new PyExpr(newExpr, PyExprUtils.pyPrecedenceForOperator(opNode.getOperator())); } - - private static String genMaybeProtect(PyExpr expr, int minSafePrecedence) { - return (expr.getPrecedence() > minSafePrecedence) ? expr.getText() : "(" + expr.getText() + ")"; - } } diff --git a/java/src/com/google/template/soy/pysrc/restricted/PyExprUtils.java b/java/src/com/google/template/soy/pysrc/restricted/PyExprUtils.java index cb1dc22a4a..72099b7a16 100644 --- a/java/src/com/google/template/soy/pysrc/restricted/PyExprUtils.java +++ b/java/src/com/google/template/soy/pysrc/restricted/PyExprUtils.java @@ -123,6 +123,24 @@ public static PyExpr genPyNotNullCheck(PyExpr pyExpr) { return new PyExpr(conditionalExpr, PyExprUtils.pyPrecedenceForOperator(Operator.NOT_EQUAL)); } + /** + * Wraps an expression with parenthesis if it's not above the minimum safe precedence. + * + *

NOTE: For the sake of brevity, this implementation loses typing information in the + * expressions. + * + * @param expr The expression to wrap. + * @param minSafePrecedence The minimum safe precedence (not inclusive). + * @return The PyExpr potentially wrapped in parenthesis. + */ + public static PyExpr maybeProtect(PyExpr expr, int minSafePrecedence) { + if (expr.getPrecedence() > minSafePrecedence) { + return expr; + } else { + return new PyExpr("(" + expr.getText() + ")", Integer.MAX_VALUE); + } + } + /** * Wraps an expression with the proper SanitizedContent constructor if contentKind is non-null. * diff --git a/java/tests/com/google/template/soy/pysrc/internal/GenPyCodeVisitorTest.java b/java/tests/com/google/template/soy/pysrc/internal/GenPyCodeVisitorTest.java index 9df6ba323b..c1772c7e73 100644 --- a/java/tests/com/google/template/soy/pysrc/internal/GenPyCodeVisitorTest.java +++ b/java/tests/com/google/template/soy/pysrc/internal/GenPyCodeVisitorTest.java @@ -37,6 +37,8 @@ /** * Unit tests for GenPyCodeVisitor. * + *

TODO(dcphillips): Add non-inlined 'if' test after adding LetNode support. + * */ public final class GenPyCodeVisitorTest extends TestCase { @@ -68,6 +70,7 @@ public final class GenPyCodeVisitorTest extends TestCase { private GenPyCodeVisitor genPyCodeVisitor; + @Override protected void setUp() { // Default to empty values for the bidi and translation configs. setupGenPyCodeVisitor("", ""); @@ -173,6 +176,7 @@ public void testMsg() { // ----------------------------------------------------------------------------------------------- // Test Utilities. + private void assertGeneratedPyFile(String soyCode, String expectedPyCode) { assertThat(getGeneratedPyFile(soyCode)).isEqualTo(expectedPyCode); } diff --git a/java/tests/com/google/template/soy/pysrc/internal/GenPyExprsVisitorTest.java b/java/tests/com/google/template/soy/pysrc/internal/GenPyExprsVisitorTest.java new file mode 100644 index 0000000000..2db1979a82 --- /dev/null +++ b/java/tests/com/google/template/soy/pysrc/internal/GenPyExprsVisitorTest.java @@ -0,0 +1,72 @@ +/* + * 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 static com.google.template.soy.pysrc.internal.PyCodeSubject.assertThatSoyCode; + +import com.google.common.collect.ImmutableList; +import com.google.template.soy.exprtree.Operator; +import com.google.template.soy.pysrc.restricted.PyExpr; +import com.google.template.soy.pysrc.restricted.PyExprUtils; + +import junit.framework.TestCase; + +/** + * Unit tests for GenPyExprsVisitor. + * + */ +public final class GenPyExprsVisitorTest extends TestCase { + + public void testRawText() { + assertThatSoyCode("I'm feeling lucky!").compilesTo( + ImmutableList.of(new PyExpr("'I\\'m feeling lucky!'", Integer.MAX_VALUE))); + } + + public void testIf() { + String soyNodeCode = + "{if $boo}\n" + + " Blah\n" + + "{elseif not $goo}\n" + + " Bleh\n" + + "{else}\n" + + " Bluh\n" + + "{/if}\n"; + String expectedPyExprText = + "'Blah' if opt_data.get('boo') else 'Bleh' if not opt_data.get('goo') else 'Bluh'"; + + assertThatSoyCode(soyNodeCode).compilesTo( + ImmutableList.of(new PyExpr(expectedPyExprText, + PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL)))); + } + + public void testIf_nested() { + String soyNodeCode = + "{if $boo}\n" + + " {if $goo}\n" + + " Blah\n" + + " {/if}\n" + + "{else}\n" + + " Bleh\n" + + "{/if}\n"; + String expectedPyExprText = + "('Blah' if opt_data.get('goo') else '') if opt_data.get('boo') else 'Bleh'"; + + assertThatSoyCode(soyNodeCode).compilesTo( + ImmutableList.of(new PyExpr(expectedPyExprText, + PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL)))); + } +} diff --git a/java/tests/com/google/template/soy/pysrc/internal/PyCodeSubject.java b/java/tests/com/google/template/soy/pysrc/internal/PyCodeSubject.java new file mode 100644 index 0000000000..e94f6f6217 --- /dev/null +++ b/java/tests/com/google/template/soy/pysrc/internal/PyCodeSubject.java @@ -0,0 +1,80 @@ +/* + * 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 static com.google.common.truth.Truth.assertAbout; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.template.soy.pysrc.internal.GenPyExprsVisitor.GenPyExprsVisitorFactory; +import com.google.template.soy.pysrc.restricted.PyExpr; +import com.google.template.soy.shared.SharedTestUtils; +import com.google.template.soy.soyparse.ParseResult; +import com.google.template.soy.soytree.SoyFileSetNode; +import com.google.template.soy.soytree.SoyNode; + +import java.util.List; + +/** + * Truth assertion which compiles the provided soy code and asserts that the generated PyExprs match + * the expected expressions. + * + */ +public final class PyCodeSubject extends Subject { + + private static final Injector INJECTOR = Guice.createInjector(new PySrcModule()); + + private GenPyExprsVisitor genPyExprsVisitor; + + + PyCodeSubject(FailureStrategy failureStrategy, String code) { + super(failureStrategy, code); + + SharedTestUtils.simulateNewApiCall(INJECTOR, null, null); + genPyExprsVisitor = INJECTOR.getInstance(GenPyExprsVisitorFactory.class).create(); + } + + public void compilesTo(List expectedPyExprs) { + ParseResult result = SharedTestUtils.parseSoyCode(getSubject()); + SoyNode node = SharedTestUtils.getNode(result.getParseTree(), 0); + + List actualPyExprs = genPyExprsVisitor.exec(node); + + assertThat(actualPyExprs).hasSize(expectedPyExprs.size()); + for (int i = 0; i < expectedPyExprs.size(); i++) { + PyExpr expectedPyExpr = expectedPyExprs.get(i); + PyExpr actualPyExpr = actualPyExprs.get(i); + assertThat(actualPyExpr.getText()).isEqualTo(expectedPyExpr.getText()); + assertThat(actualPyExpr.getPrecedence()).isEqualTo(expectedPyExpr.getPrecedence()); + } + } + + private static final SubjectFactory PYCODE = + new SubjectFactory() { + @Override public PyCodeSubject getSubject(FailureStrategy failureStrategy, String code) { + return new PyCodeSubject(failureStrategy, code); + } + }; + + public static PyCodeSubject assertThatSoyCode(String code) { + return assertAbout(PYCODE).that(code); + } +}