/* * 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.exprparse; import static com.google.common.truth.Truth.assertThat; import static com.google.template.soy.exprparse.ExpressionSubject.assertThatExpression; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.exprtree.BooleanNode; import com.google.template.soy.exprtree.ExprEquivalence; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.ExprNode.OperatorNode; import com.google.template.soy.exprtree.FieldAccessNode; import com.google.template.soy.exprtree.FloatNode; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.exprtree.GlobalNode; import com.google.template.soy.exprtree.IntegerNode; import com.google.template.soy.exprtree.ItemAccessNode; import com.google.template.soy.exprtree.ListLiteralNode; import com.google.template.soy.exprtree.MapLiteralNode; import com.google.template.soy.exprtree.NullNode; import com.google.template.soy.exprtree.Operator; import com.google.template.soy.exprtree.OperatorNodes.ConditionalOpNode; import com.google.template.soy.exprtree.OperatorNodes.EqualOpNode; import com.google.template.soy.exprtree.OperatorNodes.MinusOpNode; import com.google.template.soy.exprtree.OperatorNodes.NegativeOpNode; import com.google.template.soy.exprtree.OperatorNodes.NotOpNode; import com.google.template.soy.exprtree.OperatorNodes.NullCoalescingOpNode; import com.google.template.soy.exprtree.OperatorNodes.OrOpNode; import com.google.template.soy.exprtree.OperatorNodes.PlusOpNode; import com.google.template.soy.exprtree.OperatorNodes.TimesOpNode; import com.google.template.soy.exprtree.ProtoInitNode; import com.google.template.soy.exprtree.StringNode; import com.google.template.soy.exprtree.VarRefNode; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for the Soy expression parser. * */ @RunWith(JUnit4.class) public final class ExpressionParserTest { @Test public void testRecognizeVariable() { String[] vars = {"$aaa", "$_", "$a0b1_"}; for (String var : vars) { assertThatExpression(var).isValidVar(); } String[] nonVars = {"$", "$1", "$ aaa", "aaa", "$ij"}; for (String nonVar : nonVars) { assertThatExpression(nonVar).isNotValidVar(); } } @Test public void testRecognizeDataReference() { String[] dataRefs = { "$aaa", "$ij.aaa", "$a0a0.b1b1", "$aaa[0].bbb[12]", "$aaa[0].bbb['ccc'][$eee]", "$aaa?.bbb", "$aaa.bbb?[0]?.ccc?['ddd']", "$ij.aaa", "$aaa [1] [2] .bbb [ 3 + 4 ]['ccc']. ddd [$eee * $fff]", "functionCall($arg).field", "function.with.Dots($arg).field", "proto().field", "pro.to(a: $a).field", "['a' : 'b'].a" }; for (String dataRef : dataRefs) { assertThatExpression(dataRef).isValidExpression(); } String[] nonDataRefs = { "$", "$ aaa", "1aaa", "$1a1a", "$0", "$[12]", "$[$aaa]", "$aaa[]", "$ij[4]", "$aaa.?bbb" }; for (String nonDataRef : nonDataRefs) { assertThatExpression(nonDataRef).isNotValidExpression(); } } @Test public void testRecognizeGlobal() { String[] globals = {"aaa", "aaa.bbb.CCC", "a22 . b88_", "aaa.new", "news"}; for (String global : globals) { assertThatExpression(global).isValidGlobal(); } String[] nonGlobals = {"$aaa", "1a1a", "aaa.1a1a", "22", "aaa[33]", "aaa[bbb]", "aaa['bbb']"}; for (String nonGlobal : nonGlobals) { assertThatExpression(nonGlobal).isNotValidGlobal(); } } @Test public void testRecognizePrimitives() { // Null assertThatExpression("null").isValidExpression(); // Boolean. assertThatExpression("true").isValidExpression(); assertThatExpression("false").isValidExpression(); // Integer. (Note the negative sign is actually parsed as the unary "-" operator.) assertThatExpression("0").isValidExpression(); assertThatExpression("00").isValidExpression(); assertThatExpression("26").isValidExpression(); assertThatExpression("-729").isValidExpression(); assertThatExpression("1234567890").isValidExpression(); assertThatExpression("0x0").isValidExpression(); assertThatExpression("0x1A2B").isValidExpression(); assertThatExpression("-0xCAFE88").isValidExpression(); assertThatExpression("0x1A2b").isValidExpression(); assertThatExpression("-0xcafe88").isValidExpression(); assertThatExpression("2147483647").isValidExpression(); assertThatExpression("-2147483647").isValidExpression(); assertThatExpression("9007199254740991").isValidExpression(); assertThatExpression("-9007199254740991").isValidExpression(); assertThatExpression("0x0G").isNotValidExpression(); assertThatExpression("9007199254740992").isNotValidExpression(); // JS max safe integer + 1 assertThatExpression("-9007199254740992").isNotValidExpression(); assertThatExpression("9223372036854775808").isNotValidExpression(); // Long.MAX_VALUE + 1 // Float. (Note the negative sign is actually parsed as the unary "-" operator.) assertThatExpression("0.0").isValidExpression(); assertThatExpression("3.14159").isValidExpression(); assertThatExpression("-20.0").isValidExpression(); assertThatExpression("6.02e23").isValidExpression(); assertThatExpression("3e3").isValidExpression(); assertThatExpression("3e+3").isValidExpression(); assertThatExpression("-3e-3").isValidExpression(); assertThatExpression("0.").isNotValidExpression(); assertThatExpression(".0").isNotValidExpression(); assertThatExpression("-20.").isNotValidExpression(); assertThatExpression(".14159").isNotValidExpression(); assertThatExpression("6.02E23").isNotValidExpression(); assertThatExpression("-3E-3").isNotValidExpression(); // String. assertThatExpression("''").isValidExpression(); assertThatExpression("'{}'").isValidExpression(); assertThatExpression("'abc'").isValidExpression(); assertThatExpression("'\\\\ \\' \\\" \\n \\r \\t \\b \\f \\u00A9 \\u2468'") .isValidExpression(); assertThatExpression("'\\xA9'").isNotValidExpression(); assertThatExpression("'\\077'").isNotValidExpression(); assertThatExpression("\"\"").isNotValidExpression(); assertThatExpression("\"abc\"").isNotValidExpression(); } @Test public void testRecognizeListsAndMaps() { assertThatExpression("[]").isValidExpression(); assertThatExpression("[55]").isValidExpression(); assertThatExpression("[55,]").isValidExpression(); assertThatExpression("['blah', 123, $boo]").isValidExpression(); assertThatExpression("['blah', 123, $boo,]").isValidExpression(); assertThatExpression("[:]").isValidExpression(); assertThatExpression("['aa': 55]").isValidExpression(); assertThatExpression("['aa': 55,]").isValidExpression(); assertThatExpression("['aaa': 'blah', 'bbb': 123, $foo.bar: $boo]").isValidExpression(); assertThatExpression("['aaa': 'blah', 'bbb': 123, $foo.bar: $boo,]").isValidExpression(); } @Test public void testRecognizeDataRefAsExpression() { assertThatExpression("$aaa").isValidExpression(); assertThatExpression("$a0a0.b1b1").isValidExpression(); assertThatExpression("$aaa[0].bbb[12]").isValidExpression(); assertThatExpression("$aaa[0].bbb['ccc'][$eee]").isValidExpression(); assertThatExpression("$aaa [1] [2] .bbb [ 3 + 4 ]['ccc']. ddd [$eee * $fff]") .isValidExpression(); assertThatExpression("$").isNotValidExpression(); assertThatExpression("$ aaa").isNotValidExpression(); assertThatExpression("$1a1a").isNotValidExpression(); assertThatExpression("$0").isNotValidExpression(); assertThatExpression("$[12]").isNotValidExpression(); assertThatExpression("$[$aaa]").isNotValidExpression(); assertThatExpression("$aaa[]").isNotValidExpression(); } @Test public void testRecognizeGlobalAsExpression() { assertThatExpression("aaa").isValidExpression(); assertThatExpression("aaa.bbb.CCC").isValidExpression(); assertThatExpression("aaa.new").isValidExpression(); assertThatExpression("a22 . b88_").isValidExpression(); assertThatExpression("news").isValidExpression(); assertThatExpression("aaa[33]").isValidExpression(); // the type checker will reject assertThatExpression("aaa[bbb]").isValidExpression(); assertThatExpression("1a1a").isNotValidExpression(); assertThatExpression("aaa.1a1a").isNotValidExpression(); assertThatExpression("aaa['bbb']").isValidExpression(); } @Test public void testRecognizeFunctionCall() { assertThatExpression("isFirst($x)").isValidExpression(); assertThatExpression("isLast($y)").isValidExpression(); assertThatExpression("index($z)").isValidExpression(); assertThatExpression("randomInt()").isValidExpression(); assertThatExpression("length($x.y.z)").isValidExpression(); assertThatExpression("round(3.14159)").isValidExpression(); assertThatExpression("round(3.14159, 2)").isValidExpression(); assertThatExpression("floor(3.14)").isValidExpression(); assertThatExpression("ceiling(-8)").isValidExpression(); assertThatExpression("with(global)").isValidExpression(); assertThatExpression("with(global.fields)").isValidExpression(); assertThatExpression("dotted.fn()").isValidExpression(); assertThatExpression("nested(fn($a))").isValidExpression(); assertThatExpression("whitespace ($a)").isValidExpression(); assertThatExpression("white. spaces ($a)").isValidExpression(); assertThatExpression("proto()").isValidExpression(); assertThatExpression("pro.to()").isValidExpression(); assertThatExpression("pro .to ()").isValidExpression(); assertThatExpression("proto(a: 1, b: $foo, c: proto())").isValidExpression(); assertThatExpression("pro.to(a: 1, b: $foo, c: proto())").isValidExpression(); assertThatExpression("$isFirst()").isNotValidExpression(); assertThatExpression("$boo.isFirst()").isNotValidExpression(); assertThatExpression("proto.mixed($a, b: $b)").isNotValidExpression(); assertThatExpression("proto.mixed(a: $a, $b)").isNotValidExpression(); } @Test public void testRecognizeOperators() { // Level 8. assertThatExpression("- $a").isValidExpression(); assertThatExpression("not true").isValidExpression(); assertThatExpression("not$a").isValidExpression(); assertThatExpression("+1").isNotValidExpression(); assertThatExpression("!$a").isNotValidExpression(); // Level 7. assertThatExpression("$a * $b").isValidExpression(); assertThatExpression("5/3").isValidExpression(); assertThatExpression("5 %$x").isValidExpression(); assertThatExpression("$a * 4 / -7 % 2").isValidExpression(); // Level 6. assertThatExpression("$a+$b").isValidExpression(); assertThatExpression("7 - 12").isValidExpression(); assertThatExpression(" - 3 + 4 - 5").isValidExpression(); // Level 5. assertThatExpression("$a < 0xA00").isValidExpression(); assertThatExpression("$a>$b").isValidExpression(); assertThatExpression("3<=6").isValidExpression(); assertThatExpression("7.5>= $c").isValidExpression(); // Level 4. assertThatExpression("$a==0").isValidExpression(); assertThatExpression("-10 != $b").isValidExpression(); // Level 3. assertThatExpression("true and $b").isValidExpression(); assertThatExpression("true && $b").isNotValidExpression(); // Level 2. assertThatExpression("$a or null").isValidExpression(); assertThatExpression("$a || null").isNotValidExpression(); // Level 1. assertThatExpression("$boo?:-1").isValidExpression(); assertThatExpression("$a ?: $b ?: $c").isValidExpression(); assertThatExpression("false?4:-3").isValidExpression(); assertThatExpression("$a ? $b : $c ? $d : $e").isValidExpression(); // Parentheses. assertThatExpression("($a)").isValidExpression(); assertThatExpression("( 4- $b *$c )").isValidExpression(); } @Test public void testRecognizeAdditional() { assertThatExpression("1+2*3-4/5<6==7>=-8%9+10").isValidExpression(); assertThatExpression("((1+2)*((3)-4))/5<6==7>=-8%(9+10)").isValidExpression(); assertThatExpression("$a and true or $b or $c and false and $d or $e").isValidExpression(); assertThatExpression("$a and (true or $b) or ($c and false and ($d or $e))") .isValidExpression(); assertThatExpression("$a != 0 ? 33 : $b <= 4 ? 55 : $c ? 77 : $d").isValidExpression(); assertThatExpression("( ( $a != 0 ? 33 : $b <= 4 ) ? 55 : $c ) ? 77 : $d").isValidExpression(); assertThatExpression("round(3.14 + length($boo.foo)) != null").isValidExpression(); } @Test public void testRecognizeExpressionList() { assertThatExpression("$aaa, $bbb.ccc + 1, index($ddd)").isValidExpressionList(); assertThatExpression("").isNotValidExpressionList(); assertThatExpression("1, , 3").isNotValidExpressionList(); } @Test public void testParseVariable() { assertThatExpression("$boo").isValidVarNamed("boo"); } @Test public void testParseDataReference() throws Exception { SourceLocation loc = SourceLocation.UNKNOWN; ExprNode dataRef = assertThatExpression("$boo").isValidExpression(); assertNodeEquals(new VarRefNode("boo", loc, false, null), dataRef); dataRef = assertThatExpression("$boo.foo").isValidExpression(); assertNodeEquals( new FieldAccessNode(new VarRefNode("boo", loc, false, null), "foo", loc, false), dataRef); dataRef = assertThatExpression("$boo[0][$foo]").isValidExpression(); assertNodeEquals( new ItemAccessNode( new ItemAccessNode( new VarRefNode("boo", loc, false, null), new IntegerNode(0, loc), loc, false), new VarRefNode("foo", loc, false, null), loc, false), dataRef); dataRef = assertThatExpression("$boo?[0]?[$foo]").isValidExpression(); assertNodeEquals( new ItemAccessNode( new ItemAccessNode( new VarRefNode("boo", loc, false, null), new IntegerNode(0, loc), loc, true), new VarRefNode("foo", loc, false, null), loc, true), dataRef); dataRef = assertThatExpression("$ij.boo?[0][$ij.foo]").isValidExpression(); assertNodeEquals( new ItemAccessNode( new ItemAccessNode( new VarRefNode("boo", loc, true, null), new IntegerNode(0, loc), loc, true), new VarRefNode("foo", loc, true, null), loc, false), dataRef); } @Test public void testParseGlobal() { assertThatExpression("MOO_2").isValidGlobalNamed("MOO_2"); assertThatExpression("aaa.BBB").isValidGlobalNamed("aaa.BBB"); // Aliases are handled later, in RewriteGlobalsPass. assertThatExpression("alias.MyEnum.CCC") .withAlias("alias", "my.very.long.namespace") .isValidGlobalNamed("alias.MyEnum.CCC"); } @Test public void testParsePrimitives() throws Exception { ExprNode expr = assertThatExpression("null").isValidExpression(); assertThat(expr).isInstanceOf(NullNode.class); expr = assertThatExpression("true").isValidExpression(); assertThat(((BooleanNode) expr).getValue()).isTrue(); expr = assertThatExpression("false").isValidExpression(); assertThat(((BooleanNode) expr).getValue()).isFalse(); expr = assertThatExpression("26").isValidExpression(); assertThat(((IntegerNode) expr).getValue()).isEqualTo(26); expr = assertThatExpression("0xCAFE").isValidExpression(); assertThat(((IntegerNode) expr).getValue()).isEqualTo(0xCAFE); expr = assertThatExpression("3.14").isValidExpression(); assertThat(((FloatNode) expr).getValue()).isEqualTo(3.14); expr = assertThatExpression("3e-3").isValidExpression(); assertThat(((FloatNode) expr).getValue()).isEqualTo(3e-3); expr = assertThatExpression("'Aa`! \\n \\r \\t \\\\ \\\' \"'").isValidExpression(); assertThat(((StringNode) expr).getValue()).isEqualTo("Aa`! \n \r \t \\ \' \""); expr = assertThatExpression("'\\u2222 \\uEEEE \\u9EC4 \\u607A'").isValidExpression(); assertThat(((StringNode) expr).getValue()).isEqualTo("\u2222 \uEEEE \u9EC4 \u607A"); expr = assertThatExpression("'\u2222 \uEEEE \u9EC4 \u607A'").isValidExpression(); assertThat(((StringNode) expr).getValue()).isEqualTo("\u2222 \uEEEE \u9EC4 \u607A"); } @Test public void testParseListsAndMaps() throws Exception { ExprNode expr = assertThatExpression("[]").isValidExpression(); assertThat(((ListLiteralNode) expr).numChildren()).isEqualTo(0); expr = assertThatExpression("[55]").isValidExpression(); assertThat(((ListLiteralNode) expr).numChildren()).isEqualTo(1); expr = assertThatExpression("[55,]").isValidExpression(); assertThat(((ListLiteralNode) expr).numChildren()).isEqualTo(1); expr = assertThatExpression("['blah', 123, $boo]").isValidExpression(); assertThat(((ListLiteralNode) expr).numChildren()).isEqualTo(3); expr = assertThatExpression("['blah', 123, $boo,]").isValidExpression(); assertThat(((ListLiteralNode) expr).numChildren()).isEqualTo(3); expr = assertThatExpression("[:]").isValidExpression(); assertThat(((MapLiteralNode) expr).numChildren()).isEqualTo(0); expr = assertThatExpression("['aa': 55]").isValidExpression(); assertThat(((MapLiteralNode) expr).numChildren()).isEqualTo(2); expr = assertThatExpression("['aa': 55,]").isValidExpression(); assertThat(((MapLiteralNode) expr).numChildren()).isEqualTo(2); expr = assertThatExpression("['aaa': 'blah', 'bbb': 123, $foo.bar: $boo]").isValidExpression(); assertThat(((MapLiteralNode) expr).numChildren()).isEqualTo(6); expr = assertThatExpression("['aaa': 'blah', 'bbb': 123, $foo.bar: $boo,]").isValidExpression(); assertThat(((MapLiteralNode) expr).numChildren()).isEqualTo(6); } @Test public void testParseDataRefAsExpression() { assertThatExpression("$boo.foo").generatesASTWithRootOfType(FieldAccessNode.class); } @Test public void testParseGlobalAsExpression() throws Exception { assertThatExpression("aaa.BBB").generatesASTWithRootOfType(GlobalNode.class); } @Test public void testParseFunctionCall() throws Exception { ExprNode expr = assertThatExpression("isFirst($x)").isValidExpression(); FunctionNode isFirstFn = (FunctionNode) expr; assertThat(isFirstFn.getFunctionName()).isEqualTo("isFirst"); assertThat(isFirstFn.numChildren()).isEqualTo(1); assertThat(isFirstFn.getChild(0).toSourceString()).isEqualTo("$x"); expr = assertThatExpression("round(3.14159, 2)").isValidExpression(); FunctionNode roundFn = (FunctionNode) expr; assertThat(roundFn.getFunctionName()).isEqualTo("round"); assertThat(roundFn.numChildren()).isEqualTo(2); assertThat(((FloatNode) roundFn.getChild(0)).getValue()).isEqualTo(3.14159); assertThat(((IntegerNode) roundFn.getChild(1)).getValue()).isEqualTo(2); } @Test public void testParseProtoInitCall() throws Exception { ExprNode expr = assertThatExpression("my.Proto(a: 1, b: glo.bal, c: fn('str'))").isValidExpression(); ProtoInitNode protoFn = (ProtoInitNode) expr; assertThat(protoFn.getProtoName()).isEqualTo("my.Proto"); assertThat(protoFn.getParamNames()).containsExactly("a", "b", "c").inOrder(); assertThat(protoFn.numChildren()).isEqualTo(3); assertThat(((IntegerNode) protoFn.getChild(0)).getValue()).isEqualTo(1); assertThat(((GlobalNode) protoFn.getChild(1)).getName()).isEqualTo("glo.bal"); assertThat(((StringNode) ((FunctionNode) protoFn.getChild(2)).getChild(0)).getValue()) .isEqualTo("str"); } @Test public void testParseOperators() throws Exception { ExprNode expr = assertThatExpression("-11").isValidExpression(); NegativeOpNode negOp = (NegativeOpNode) expr; assertThat(((IntegerNode) negOp.getChild(0)).getValue()).isEqualTo(11); expr = assertThatExpression("not false").isValidExpression(); NotOpNode notOp = (NotOpNode) expr; assertThat(((BooleanNode) notOp.getChild(0)).getValue()).isFalse(); expr = assertThatExpression("90 -14.75").isValidExpression(); MinusOpNode minusOp = (MinusOpNode) expr; assertThat(((IntegerNode) minusOp.getChild(0)).getValue()).isEqualTo(90); assertThat(((FloatNode) minusOp.getChild(1)).getValue()).isEqualTo(14.75); expr = assertThatExpression("$a or true").isValidExpression(); OrOpNode orOp = (OrOpNode) expr; assertThat(orOp.getChild(0).toSourceString()).isEqualTo("$a"); assertThat(((BooleanNode) orOp.getChild(1)).getValue()).isTrue(); expr = assertThatExpression("$a ?: $b ?: $c").isValidExpression(); NullCoalescingOpNode nullCoalOp0 = (NullCoalescingOpNode) expr; assertThat(nullCoalOp0.getChild(0).toSourceString()).isEqualTo("$a"); NullCoalescingOpNode nullCoalOp1 = (NullCoalescingOpNode) nullCoalOp0.getChild(1); assertThat(nullCoalOp1.getChild(0).toSourceString()).isEqualTo("$b"); assertThat(nullCoalOp1.getChild(1).toSourceString()).isEqualTo("$c"); expr = assertThatExpression("$a?:$b==null?0*1:0x1").isValidExpression(); NullCoalescingOpNode nullCoalOp = (NullCoalescingOpNode) expr; assertThat(nullCoalOp.getChild(0).toSourceString()).isEqualTo("$a"); ConditionalOpNode condOp = (ConditionalOpNode) nullCoalOp.getChild(1); assertThat(condOp.getChild(0)).isInstanceOf(EqualOpNode.class); assertThat(condOp.getChild(1)).isInstanceOf(TimesOpNode.class); assertThat(condOp.getChild(2)).isInstanceOf(IntegerNode.class); } @Test public void testParseExpressionList() throws Exception { List<ExprNode> exprList = assertThatExpression("$aaa, $bbb.ccc + 1, index($ddd)").isValidExpressionList(); assertThat(exprList).hasSize(3); assertThat(exprList.get(0)).isInstanceOf(VarRefNode.class); assertThat(exprList.get(1)).isInstanceOf(PlusOpNode.class); assertThat(exprList.get(2)).isInstanceOf(FunctionNode.class); } @Test public void testOperatorPrecedence() throws Exception { // + is left associative assertThat(precedenceString("1 + 2")).isEqualTo("1 + 2"); assertThat(precedenceString("1 + 2 + 3")).isEqualTo("(1 + 2) + 3"); assertThat(precedenceString("1 + 2 + 3 + 4 + 5 + 6")) .isEqualTo("((((1 + 2) + 3) + 4) + 5) + 6"); // ?: is right associative assertThat(precedenceString("$a ?: $b ?: $c")).isEqualTo("$a ?: ($b ?: $c)"); // ternary is right associative (though still confusing) assertThat(precedenceString("$a ? $b ? $c : $d : $e ? $f : $g")) .isEqualTo("$a ? ($b ? $c : $d) : ($e ? $f : $g)"); // unary negation ?: is right associative assertThat(precedenceString("- - $a")).isEqualTo("- (- $a)"); // all together now! assertThat(precedenceString("1 + - 2 * 3 + 4 % 2 ?: 3")) .isEqualTo("((1 + ((- 2) * 3)) + (4 % 2)) ?: 3"); assertThat(precedenceString("-$a.b > 0 ? $c.d : $c")).isEqualTo("((- $a.b) > 0) ? $c.d : $c"); } // Parses the soy expression and then prints it with copious parens to indicate the associativity private String precedenceString(String soyExpr) { ExprNode node = assertThatExpression(soyExpr).isValidExpression(); return formatNode(node, true); } private String formatNode(ExprNode node, boolean outermost) { if (node instanceof OperatorNode) { OperatorNode opNode = (OperatorNode) node; String formatted = formatOperator(opNode); if (!outermost) { return "(" + formatted + ")"; } return formatted; } else { return node.toSourceString(); } } private String formatOperator(OperatorNode opNode) { Operator op = opNode.getOperator(); switch (op) { case NEGATIVE: case NOT: // unary return op.getTokenString() + " " + formatNode(opNode.getChild(0), false); case CONDITIONAL: return formatNode(opNode.getChild(0), false) + " ? " + formatNode(opNode.getChild(1), false) + " : " + formatNode(opNode.getChild(2), false); default: // everything else is binary assertEquals(2, op.getNumOperands()); return formatNode(opNode.getChild(0), false) + " " + op.getTokenString() + " " + formatNode(opNode.getChild(1), false); } } // ----------------------------------------------------------------------------------------------- // Helpers. private static void assertNodeEquals(ExprNode expected, ExprNode actual) { if (!ExprEquivalence.get().equivalent(expected, actual)) { fail( String.format( "Expected <%s> but was: <%s>", expected.toSourceString(), actual.toSourceString())); } } }