/* * 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.jbcsrc; import static com.google.template.soy.jbcsrc.BytecodeUtils.STRING_TYPE; import static com.google.template.soy.jbcsrc.BytecodeUtils.constant; import static com.google.template.soy.jbcsrc.BytecodeUtils.constantNull; import static com.google.template.soy.jbcsrc.FieldRef.staticFieldReference; import static com.google.template.soy.jbcsrc.TemplateTester.assertThatTemplateBody; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.data.SanitizedContent; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.SanitizedContents; import com.google.template.soy.data.SoyDataException; import com.google.template.soy.data.SoyDict; import com.google.template.soy.data.SoyList; import com.google.template.soy.data.SoyMap; import com.google.template.soy.data.SoyValue; import com.google.template.soy.data.SoyValueConverter; import com.google.template.soy.data.internal.DictImpl; import com.google.template.soy.data.restricted.BooleanData; import com.google.template.soy.data.restricted.FloatData; import com.google.template.soy.data.restricted.IntegerData; import com.google.template.soy.data.restricted.StringData; import com.google.template.soy.exprparse.ExpressionParser; import com.google.template.soy.exprparse.SoyParsingContext; import com.google.template.soy.exprtree.AbstractExprNodeVisitor; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.ExprNode.ParentExprNode; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.exprtree.VarRefNode; import com.google.template.soy.jbcsrc.ExpressionTester.ExpressionSubject; import com.google.template.soy.jbcsrc.TemplateTester.CompiledTemplateSubject; import com.google.template.soy.jbcsrc.api.AdvisingAppendable; import com.google.template.soy.jbcsrc.shared.CompiledTemplate; import com.google.template.soy.jbcsrc.shared.RenderContext; import com.google.template.soy.shared.restricted.SoyFunction; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.defn.LocalVar; import com.google.template.soy.soytree.defn.TemplateParam; import com.google.template.soy.types.SoyType; import com.google.template.soy.types.SoyTypes; import com.google.template.soy.types.aggregate.ListType; import com.google.template.soy.types.aggregate.MapType; import com.google.template.soy.types.aggregate.RecordType; import com.google.template.soy.types.primitive.FloatType; import com.google.template.soy.types.primitive.IntType; import com.google.template.soy.types.primitive.SanitizedType; import com.google.template.soy.types.primitive.StringType; import com.google.template.soy.types.primitive.UnknownType; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.objectweb.asm.Label; import org.objectweb.asm.Type; import org.objectweb.asm.commons.Method; /** Tests for {@link ExpressionCompiler} */ @RunWith(JUnit4.class) public class ExpressionCompilerTest { private final Map<String, SoyExpression> variables = new HashMap<>(); private final ExpressionCompiler testExpressionCompiler = ExpressionCompiler.create( new ExpressionDetacher.Factory() { @Override public ExpressionDetacher createExpressionDetacher(Label label) { return new ExpressionDetacher() { @Override public Expression resolveSoyValueProvider(Expression soyValueProvider) { if (variables.containsValue(soyValueProvider)) { // This is hacky, but our variables are not SVPs, just SoyValues. This is // inconsistent with reality but makes the tests easier to write. // A better solution may be to have the variables map just hold expressions for // SoyValueProviders, but that is annoying. return soyValueProvider; } return MethodRef.SOY_VALUE_PROVIDER_RESOLVE.invoke(soyValueProvider); } @Override public Expression resolveSoyValueProviderList(Expression soyValueProviderList) { throw new UnsupportedOperationException(); } }; } }, new TemplateParameterLookup() { @Override public Expression getParam(TemplateParam paramName) { return variables.get(paramName.name()); } @Override public Expression getLocal(SyntheticVarName varName) { throw new UnsupportedOperationException(); } @Override public Expression getLocal(LocalVar localName) { throw new UnsupportedOperationException(); } @Override public Expression getRenderContext() { return staticFieldReference(ExpressionCompiler.class, "currentRenderContext") .accessor(); } @Override public Expression getParamsRecord() { throw new UnsupportedOperationException(); } @Override public Expression getIjRecord() { throw new UnsupportedOperationException(); } }, new TemplateVariableManager( JbcSrcNameGenerators.forFieldNames(), null, null, getRenderMethod())); private static Method getRenderMethod() { try { return Method.getMethod( CompiledTemplate.class.getMethod( "render", AdvisingAppendable.class, RenderContext.class)); } catch (Exception e) { throw new RuntimeException(e); } } @Before public void setUp() { variables.clear(); } @Test public void testConstants() { assertExpression("1").evaluatesTo(1L); assertExpression("1.0").evaluatesTo(1D); assertExpression("false").evaluatesTo(false); assertExpression("true").evaluatesTo(true); assertExpression("'asdf'").evaluatesTo("asdf"); } @Test public void testCollectionLiterals_list() { assertExpression("[]").evaluatesTo(ImmutableList.of()); // Lists values are always boxed assertExpression("[1, 1.0, 'asdf', false]") .evaluatesTo( ImmutableList.of( IntegerData.forValue(1), FloatData.forValue(1.0), StringData.forValue("asdf"), BooleanData.FALSE)); } @Test public void testCollectionLiterals_map() { assertExpression("[:]").evaluatesTo(SoyValueConverter.EMPTY_DICT); // Map values are always boxed. SoyMaps use == for equality, so check equivalence by comparing // their string representations. assertExpression("['a': 1, 'b': 1.0, 'c': 'asdf', 'd': false] + ''") .evaluatesTo( DictImpl.forProviderMap( ImmutableMap.<String, SoyValue>of( "a", IntegerData.forValue(1), "b", FloatData.forValue(1.0), "c", StringData.forValue("asdf"), "d", BooleanData.FALSE)) .toString()); } @Test public void testNegativeOpNode() { assertExpression("-1").evaluatesTo(-1L); assertExpression("-1.0").evaluatesTo(-1.0); // TODO(user): this should be rejected by the type checker assertExpression("-'asdf'").throwsException(SoyDataException.class); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forInt(constant(1L)))); assertExpression("-$foo").evaluatesTo(IntegerData.forValue(-1)); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(1D)))); assertExpression("-$foo").evaluatesTo(FloatData.forValue(-1.0)); } @Test public void testModOpNode() { assertExpression("3 % 2").evaluatesTo(1L); assertExpression("5 % 3").evaluatesTo(2L); // TODO(user): the soy type checker should flag this, but it doesn't. try { compileExpression("5.0 % 3.0"); fail(); } catch (Exception expected) { } variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forInt(constant(3L)))); variables.put("bar", untypedBoxedSoyExpression(SoyExpression.forInt(constant(2L)))); assertExpression("$foo % $bar").evaluatesTo(1L); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(3.0)))); variables.put("bar", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(2.0)))); assertExpression("$foo % $bar").throwsException(SoyDataException.class); } @Test public void testDivideByOpNode() { assertExpression("3 / 2").evaluatesTo(1.5); // note the coercion to floating point assertExpression("4.2 / 2").evaluatesTo(2.1); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(3.0)))); variables.put("bar", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(2.0)))); assertExpression("$foo / $bar").evaluatesTo(1.5); } @Test public void testTimesOpNode() { assertExpression("4.2 * 2").evaluatesTo(8.4); assertExpression("4 * 2").evaluatesTo(8L); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(3.0)))); variables.put("bar", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(2.0)))); assertExpression("$foo * $bar").evaluatesTo(FloatData.forValue(6.0)); } @Test public void testMinusOpNode() { assertExpression("4.2 - 2").evaluatesTo(2.2); assertExpression("4 - 2").evaluatesTo(2L); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(3.0)))); variables.put("bar", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(2.0)))); assertExpression("$foo - $bar").evaluatesTo(FloatData.forValue(1.0)); } @Test public void testPlusOpNode() { assertExpression("4.2 + 2").evaluatesTo(6.2); assertExpression("4 + 2").evaluatesTo(6L); assertExpression("4 + '2'").evaluatesTo("42"); assertExpression("'4' + 2").evaluatesTo("42"); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forString(constant("foo")))); assertExpression("$foo + 2").evaluatesTo(StringData.forValue("foo2")); assertExpression("$foo + '2'").evaluatesTo("foo2"); // Note, not boxed variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forInt(constant(1L)))); assertExpression("$foo + 2").evaluatesTo(IntegerData.forValue(3)); assertExpression("$foo + '2'").evaluatesTo("12"); assertExpression("['foo'] + ['bar']").evaluatesTo("[foo][bar]"); } @Test public void testNotOpNode() { assertExpression("not false").evaluatesTo(true); assertExpression("not true").evaluatesTo(false); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forString(constant("foo")))); assertExpression("not $foo").evaluatesTo(false); variables.put("foo", untypedBoxedSoyExpression(SoyExpression.forString(constant("")))); assertExpression("not $foo").evaluatesTo(true); // empty string is falsy } @Test public void testComparisonOperators() { variables.put("oneInt", untypedBoxedSoyExpression(SoyExpression.forInt(constant(1L)))); variables.put("oneFloat", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(1.0)))); variables.put("twoInt", untypedBoxedSoyExpression(SoyExpression.forInt(constant(2L)))); variables.put("twoFloat", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(2.0)))); for (String one : ImmutableList.of("1", "1.0", "$oneInt", "$oneFloat")) { for (String two : ImmutableList.of("2", "2.0", "$twoInt", "$twoFloat")) { assertExpression(one + " < " + two).evaluatesTo(true); assertExpression(one + " < " + one).evaluatesTo(false); assertExpression(two + " < " + one).evaluatesTo(false); assertExpression(one + " <= " + two).evaluatesTo(true); assertExpression(one + " <= " + one).evaluatesTo(true); assertExpression(two + " <= " + one).evaluatesTo(false); assertExpression(one + " > " + two).evaluatesTo(false); assertExpression(one + " > " + one).evaluatesTo(false); assertExpression(two + " > " + one).evaluatesTo(true); assertExpression(one + " >= " + two).evaluatesTo(false); assertExpression(one + " >= " + one).evaluatesTo(true); assertExpression(two + " >= " + one).evaluatesTo(true); assertExpression(one + " == " + two).evaluatesTo(false); assertExpression(one + " == " + one).evaluatesTo(true); assertExpression(two + " == " + one).evaluatesTo(false); assertExpression(one + " != " + two).evaluatesTo(true); assertExpression(one + " != " + one).evaluatesTo(false); assertExpression(two + " != " + one).evaluatesTo(true); } } } @Test public void testConditionalOperators() { variables.put("true", untypedBoxedSoyExpression(SoyExpression.TRUE)); variables.put("false", untypedBoxedSoyExpression(SoyExpression.FALSE)); for (String trueExpr : ImmutableList.of("true", "$true")) { for (String falseExpr : ImmutableList.of("false", "$false")) { assertExpression(falseExpr + " or " + falseExpr).evaluatesTo(false); assertExpression(falseExpr + " or " + trueExpr).evaluatesTo(true); assertExpression(trueExpr + " or " + falseExpr).evaluatesTo(true); assertExpression(trueExpr + " or " + trueExpr).evaluatesTo(true); assertExpression(falseExpr + " and " + falseExpr).evaluatesTo(false); assertExpression(falseExpr + " and " + trueExpr).evaluatesTo(false); assertExpression(trueExpr + " and " + falseExpr).evaluatesTo(false); assertExpression(trueExpr + " and " + trueExpr).evaluatesTo(true); } } } // The arithmetic types are handled by testComparisonOperators, the == and != operators have // extra semantics for strings as well as boxed fallback @Test public void testEqualOpNode() { assertExprNotEquals("'asdf'", "12.0"); assertExprEquals("'12'", "12.0"); assertExprEquals("'12.0'", "12.0"); assertExprEquals("'12.0'", "'12.0'"); assertExprEquals("'asdf'", "'asdf'"); variables.put("str", untypedBoxedSoyExpression(SoyExpression.forString(constant("foo")))); assertExprEquals("$str", "'foo'"); assertExprNotEquals("$str", "'bar'"); variables.put("intStr", untypedBoxedSoyExpression(SoyExpression.forString(constant("12")))); assertExprEquals("$intStr", "'12'"); assertExprEquals("$intStr", "12"); assertExprNotEquals("$intStr", "'bar'"); variables.put("floatStr", untypedBoxedSoyExpression(SoyExpression.forFloat(constant(12.0)))); assertExprEquals("$floatStr", "'12'"); assertExprEquals("$floatStr", "12"); assertExprNotEquals("$floatStr", "'bar'"); assertExprEquals("null", "null"); assertExprNotEquals("'a'", "null"); assertExprNotEquals("null", "'a'"); } @Test public void testConditionalOpNode() { assertExpression("false ? 1 : 2").evaluatesTo(2L); assertExpression("true ? 1 : 2").evaluatesTo(1L); assertExpression("false ? 1.0 : 2").evaluatesTo(IntegerData.forValue(2)); assertExpression("true ? 1 : 2.0").evaluatesTo(IntegerData.forValue(1)); assertExpression("false ? 'a' : 'b'").evaluatesTo("b"); assertExpression("true ? 'a' : 'b'").evaluatesTo("a"); // note the boxing assertExpression("false ? 'a' : 2").evaluatesTo(IntegerData.forValue(2)); assertExpression("true ? 1 : 'b'").evaluatesTo(IntegerData.forValue(1)); assertExpression("false ? 1 : 'b'").evaluatesTo(StringData.forValue("b")); assertExpression("true ? 'a' : 2").evaluatesTo(StringData.forValue("a")); } // conditional op expression have had a number of bugs due previous implementations that // aggressively unboxed operands @Test public void testConditionalOpNode_advanced() { CompiledTemplateSubject tester = assertThatTemplateBody("{@param? p : string}", "{$p ? $p : '' }"); tester.rendersAs("", ImmutableMap.<String, Object>of()); tester.rendersAs("hello", ImmutableMap.<String, Object>of("p", "hello")); tester = assertThatTemplateBody( "{@param? p : map<string, string>}", "{if $p}", " {$p['key']}", "{/if}"); tester.rendersAs("", ImmutableMap.<String, Object>of()); tester = assertThatTemplateBody("{@param? p : string}", "{$p ? $p : 1 }"); tester.rendersAs("1", ImmutableMap.<String, Object>of()); tester.rendersAs("hello", ImmutableMap.<String, Object>of("p", "hello")); tester = assertThatTemplateBody("{@param p : int}", "{$p ? 1 : $p }"); tester.rendersAs("0", ImmutableMap.<String, Object>of("p", 0)); tester.rendersAs("1", ImmutableMap.<String, Object>of("p", 2)); tester = assertThatTemplateBody( "{@param b : bool}", "{@param v : list<int>}", "{$b ? $v[0] : $v[1] + 1}"); tester.rendersAs("null", ImmutableMap.<String, Object>of("b", true, "v", Arrays.asList())); tester.rendersAs("3", ImmutableMap.<String, Object>of("b", false, "v", Arrays.asList(1, 2))); } @Test public void testNullCoalescingOpNode() { assertExpression("1 ?: 2").evaluatesTo(1L); // force the type checker to interpret the left hand side as a nullable string, the literal null // is rejected by the type checker. assertExpression("(true ? null : 'a') ?: 2").evaluatesTo(IntegerData.forValue(2)); assertExpression("(true ? null : 'a') ?: 'b'").evaluatesTo("b"); assertExpression("(false ? null : 'a') ?: 'b'").evaluatesTo("a"); variables.put( "p1", untypedBoxedSoyExpression(SoyExpression.forString(constantNull(STRING_TYPE)))); variables.put("p2", SoyExpression.forString(constant("a")).box()); assertExpression("$p1 ?: $p2").evaluatesTo("a"); SoyType htmlType = SanitizedType.getTypeForContentKind(ContentKind.HTML); variables.put( "p1", SoyExpression.forSoyValue( htmlType, MethodRef.ORDAIN_AS_SAFE.invoke(constant("<b>hello</b>"), constant(ContentKind.HTML)))); variables.put("p2", SoyExpression.forString(constant("")).box()); assertExpression("$p1 ?: $p2").evaluatesTo(SanitizedContents.constantHtml("<b>hello</b>")); variables.put( "p1", SoyExpression.forSoyValue(htmlType, constantNull(Type.getType(SanitizedContent.class))) .asNullable()); assertExpression("$p1 ?: $p2").evaluatesTo(""); } // null coalescing op expression have had a number of bugs due to the advanced unboxing // conversions forcing unnecessary NullPointerExceptions @Test public void testNullCoalescingOpNode_advanced() { CompiledTemplateSubject tester = assertThatTemplateBody("{@param v : list<string>}", "{$v[0] ?: $v[1] }"); tester.rendersAs("null", ImmutableMap.<String, Object>of("v", Arrays.asList())); tester.rendersAs("b", ImmutableMap.<String, Object>of("v", Arrays.asList(null, "b"))); tester.rendersAs("a", ImmutableMap.<String, Object>of("v", Arrays.asList("a", "b"))); } @Test public void testCheckNotNull() { assertExpression("checkNotNull(1 < 2 ? null : 'a')") .throwsException(NullPointerException.class, "'1 < 2 ? null : 'a'' evaluates to null"); assertExpression("checkNotNull('a')").evaluatesTo("a"); } @Test public void testItemAccess_lists() { variables.put("list", compileExpression("[0, 1, 2]").box()); // By default all values are boxed assertExpression("$list[0]").evaluatesTo(IntegerData.forValue(0)); assertExpression("$list[1]").evaluatesTo(IntegerData.forValue(1)); assertExpression("$list[2]").evaluatesTo(IntegerData.forValue(2)); // However, they will be unboxed if possible assertExpression("$list[0] + 1").evaluatesTo(1L); // null, not IndexOutOfBoundsException assertExpression("$list[3]").evaluatesTo(null); assertExpression("$list[3] + 1").throwsException(NullPointerException.class); // even if the index type is not known, it still works variables.put("anInt", untypedBoxedSoyExpression(SoyExpression.forInt(constant(1L)))); assertExpression("$list[$anInt]").evaluatesTo(IntegerData.forValue(1)); // And we can still unbox the return value assertExpression("$list[$anInt] + 1").evaluatesTo(2L); variables.put("anInt", untypedBoxedSoyExpression(SoyExpression.forInt(constant(3L)))); assertExpression("$list[$anInt]").evaluatesTo(null); } @Test public void testItemAccess_maps() { // String literal keys trigger a heuristic that makes MapLiteral mean RecordLiteral and you // can't use 'item access' to read it. We use string concatention to force a map interpretation. variables.put("map", compileExpression("['a' + '' : 0, 'b': 1, 'c' : 2]").box()); // By default all values are boxed assertExpression("$map['a']").evaluatesTo(IntegerData.forValue(0)); assertExpression("$map['b']").evaluatesTo(IntegerData.forValue(1)); assertExpression("$map['c']").evaluatesTo(IntegerData.forValue(2)); assertExpression("$map['not valid']").evaluatesTo(null); } @Test public void testNullSafeItemAccess_map() { // Note: due to bugs in the type resolver (b/20537225) we can't properly type this variable // so instead we have to lie about the nullability of this map. variables.put( "nullMap", SoyExpression.forSoyValue( MapType.of(StringType.getInstance(), IntType.getInstance()), BytecodeUtils.constantNull(Type.getType(SoyMap.class)))); assertExpression("$nullMap['a']").throwsException(NullPointerException.class); assertExpression("$nullMap?['a']").evaluatesTo(null); } @Test public void testNullSafeItemAccess_list() { variables.put( "nullList", SoyExpression.forSoyValue( ListType.of(StringType.getInstance()), BytecodeUtils.constantNull(Type.getType(SoyList.class)))); assertExpression("$nullList[1]") .doesNotContainCode("IFNULL") .doesNotContainCode("IFNNONULL") // no null checks .throwsException(NullPointerException.class); assertExpression("$nullList?[1]").evaluatesTo(null); } @Test public void testFieldAccess() { variables.put("record", compileExpression("['a': 0, 'b': 1, 'c': 2]").box()); // By default all values are boxed assertExpression("$record.a").evaluatesTo(IntegerData.forValue(0)); assertExpression("$record.b").evaluatesTo(IntegerData.forValue(1)); assertExpression("$record.c").evaluatesTo(IntegerData.forValue(2)); // However, they will be unboxed if possible assertExpression("$record.a + 1").evaluatesTo(1L); } @Test public void testNullSafeFieldAccess() { variables.put( "nullRecord", SoyExpression.forSoyValue( SoyTypes.makeNullable(RecordType.of(ImmutableMap.of("a", StringType.getInstance()))), BytecodeUtils.constantNull(Type.getType(SoyDict.class)))); assertExpression("$nullRecord.a").throwsException(NullPointerException.class); assertExpression("$nullRecord?.a").evaluatesTo(null); } @Test public void testBuiltinFunctions() { variables.put("x", compileExpression("['a': 1]").box()); variables.put( "y", SoyExpression.forSoyValue( SoyTypes.makeNullable(FloatType.getInstance()), BytecodeUtils.constantNull(Type.getType(FloatData.class)))); assertExpression("checkNotNull($x.a)").evaluatesTo(IntegerData.forValue(1)); assertExpression("checkNotNull($y)").throwsException(NullPointerException.class); } @Test public void testMaxAndMin() { assertExpression("min(2, 3)").evaluatesTo(2L); assertExpression("max(2, 3)").evaluatesTo(3L); assertExpression("min(0.1, 1.1)").evaluatesTo(0.1); assertExpression("max(0.1, 1.1)").evaluatesTo(1.1); } private void assertExprEquals(String left, String right) { assertExpression(left + " == " + right).evaluatesTo(true); assertExpression(left + " != " + right).evaluatesTo(false); } private void assertExprNotEquals(String left, String right) { assertExpression(left + " == " + right).evaluatesTo(false); assertExpression(left + " != " + right).evaluatesTo(true); } private ExpressionSubject assertExpression(String soyExpr) { SoyExpression compile = compileExpression(soyExpr); return ExpressionTester.assertThatExpression(compile); } private SoyExpression compileExpression(String soyExpr) { // The fake function allows us to work around the 'can't print bool' restrictions String createTemplateBody = createTemplateBody("fakeFunction(" + soyExpr + ")"); PrintNode code = (PrintNode) SoyFileSetParserBuilder.forTemplateContents(createTemplateBody) .addSoyFunction( new SoyFunction() { @Override public String getName() { return "fakeFunction"; } @Override public Set<Integer> getValidArgsSizes() { return ImmutableSet.of(1); } }) .parse() .fileSet() .getChild(0) .getChild(0) .getChild(0); return testExpressionCompiler.compile(((FunctionNode) code.getExpr().getChild(0)).getChild(0)); } private String createTemplateBody(String soyExpr) { // collect all varrefs and apply them as template parameters. This way all varrefs have a valid // vardef // TODO(lukes): this logic would be useful in a lot of tests and potentially unblock efforts to // eliminate UNDECLARED vars ExprNode expr = new ExpressionParser(soyExpr, SourceLocation.UNKNOWN, SoyParsingContext.exploding()) .parseExpression(); final StringBuilder templateBody = new StringBuilder(); new AbstractExprNodeVisitor<Void>() { final Set<String> names = new HashSet<>(); @Override protected void visitVarRefNode(VarRefNode node) { if (names.add(node.getName())) { SoyType type = variables.get(node.getName()).soyType(); templateBody .append("{@param ") .append(node.getName()) .append(": ") .append(type) .append("}\n"); } } @Override protected void visitExprNode(ExprNode node) { if (node instanceof ParentExprNode) { visitChildren((ParentExprNode) node); } } }.exec(expr); templateBody.append("{" + soyExpr + "}\n"); return templateBody.toString(); } /** * This helper can take a SoyExpression and essentially strip type information from it, this is * useful for testing fallback implementations in the compiler. */ private SoyExpression untypedBoxedSoyExpression(final SoyExpression expr) { return SoyExpression.forSoyValue(UnknownType.getInstance(), expr.box()); } }