/*
* 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.jssrc.internal;
import static com.google.template.soy.exprtree.Operator.CONDITIONAL;
import static com.google.template.soy.exprtree.Operator.NULL_COALESCING;
import static com.google.template.soy.exprtree.Operator.OR;
import static com.google.template.soy.exprtree.Operator.PLUS;
import static com.google.template.soy.jssrc.dsl.CodeChunk.id;
import static com.google.template.soy.jssrc.dsl.CodeChunk.number;
import static com.google.template.soy.jssrc.internal.JsSrcSubject.assertThatSoyExpr;
import static com.google.template.soy.jssrc.internal.JsSrcSubject.assertThatSoyFile;
import com.google.common.collect.ImmutableMap;
import com.google.template.soy.basetree.SyntaxVersion;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link TranslateExprNodeVisitor}.
*
*/
@RunWith(JUnit4.class)
public final class TranslateExprNodeVisitorTest {
// Let 'goo' simulate a local variable from a 'foreach' loop.
private static final ImmutableMap<String, CodeChunk.WithValue> LOCAL_VAR_TRANSLATIONS =
ImmutableMap.<String, CodeChunk.WithValue>builder()
.put("goo", id("gooData8"))
.put("goo__isFirst", id("gooIndex8").doubleEquals(number(0)))
.put("goo__isLast", id("gooIndex8").doubleEquals(id("gooListLen8").minus(number(1))))
.put("goo__index", id("gooIndex8"))
.build();
@Test
public void testStringLiteral() {
assertThatSoyExpr("'waldo'").generatesCode("'waldo'");
// Ensure Unicode gets escaped, since there's no guarantee about the output encoding of the JS.
assertThatSoyExpr("'\u05E9\u05DC\u05D5\u05DD'").generatesCode("'\\u05E9\\u05DC\\u05D5\\u05DD'");
}
@Test
public void testListLiteral() {
assertThatSoyExpr("['blah', 123, $foo]").generatesCode("['blah', 123, opt_data.foo]");
assertThatSoyExpr("[]").generatesCode("[]");
}
@Test
public void testMapLiteral() {
// ------ Unquoted keys. ------
assertThatSoyExpr("[:]").generatesCode("{}");
assertThatSoyExpr("['aaa': 123, 'bbb': 'blah']").generatesCode("{aaa: 123, bbb: 'blah'}");
assertThatSoyExpr("['aaa': $foo, 'bbb': 'blah']")
.generatesCode("{aaa: opt_data.foo, bbb: 'blah'}");
assertThatSoyExpr("['aaa': ['bbb': 'blah']]").generatesCode("{aaa: {bbb: 'blah'}}");
// ------ Quoted keys. ------
assertThatSoyExpr("quoteKeysIfJs([:])").generatesCode("{}");
assertThatSoyExpr("quoteKeysIfJs( ['aaa': $foo, 'bbb': 'blah'] )")
.generatesCode("{'aaa': opt_data.foo, 'bbb': 'blah'}");
assertThatSoyExpr("quoteKeysIfJs(['aaa': 123, $boo: $foo])")
.generatesCode(
"var $tmp = {'aaa': 123};", "$tmp[soy.$$checkMapKey(opt_data.boo)] = opt_data.foo;");
assertThatSoyExpr("quoteKeysIfJs([$boo: $foo, $goo[0]: 123])")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode(
"var $tmp = {};",
"$tmp[soy.$$checkMapKey(opt_data.boo)] = opt_data.foo;",
"$tmp[soy.$$checkMapKey(gooData8[0])] = 123;");
assertThatSoyExpr("quoteKeysIfJs(['aaa': ['bbb': 'blah']])")
.generatesCode("{'aaa': {bbb: 'blah'}}");
// ------ Errors. ------
// Non-string key is error.
assertThatSoyExpr("[0: 123, 1: 'oops']")
.causesErrors(
"Keys in map literals cannot be constants (found constant '0').",
"Keys in map literals cannot be constants (found constant '1').");
SoyJsSrcOptions noCompiler = new SoyJsSrcOptions();
SoyJsSrcOptions withCompiler = new SoyJsSrcOptions();
withCompiler.setShouldGenerateJsdoc(true);
// Non-identifier key without quoteKeysIfJs() is error only when using Closure Compiler.
assertThatSoyExpr("['0': 123, '1': $foo]")
.withJsSrcOptions(noCompiler)
.generatesCode("{'0': 123, '1': opt_data.foo}");
assertThatSoyExpr("['0': 123, '1': '123']")
.withJsSrcOptions(withCompiler)
.causesErrors(
"Map literal with non-identifier key '0' must be wrapped in quoteKeysIfJs().",
"Map literal with non-identifier key '1' must be wrapped in quoteKeysIfJs().");
// Expression key without quoteKeysIfJs() is error only when using Closure Compiler.
assertThatSoyExpr("['aaa': 123, $boo: $foo]")
.withJsSrcOptions(noCompiler)
.generatesCode(
"var $tmp = {aaa: 123};", "$tmp[soy.$$checkMapKey(opt_data.boo)] = opt_data.foo;");
assertThatSoyExpr("['aaa': 123, $boo: $foo, $moo: $goo]")
.withJsSrcOptions(withCompiler)
.causesErrors(
"Expression key '$boo' in map literal must be wrapped in quoteKeysIfJs().",
"Expression key '$moo' in map literal must be wrapped in quoteKeysIfJs().");
}
@Test
public void testDataRef() {
assertThatSoyExpr("$boo").generatesCode("opt_data.boo");
assertThatSoyExpr("$boo.goo").generatesCode("opt_data.boo.goo");
assertThatSoyExpr("$goo")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("gooData8");
assertThatSoyExpr("$goo.boo")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("gooData8.boo");
assertThatSoyExpr("$boo[0][1].foo[2]").generatesCode("opt_data.boo[0][1].foo[2]");
assertThatSoyExpr("$boo[0][1]").generatesCode("opt_data.boo[0][1]");
assertThatSoyExpr("$boo[$foo][$goo+1]")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("opt_data.boo[opt_data.foo][gooData8 + 1]");
assertThatSoyExpr("$class").generatesCode("opt_data.class");
assertThatSoyExpr("$boo.yield").generatesCode("opt_data.boo.yield");
assertThatSoyExpr("$boo?.goo")
.generatesCode("opt_data.boo == null ? null : opt_data.boo.goo")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$goo?.boo")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("gooData8 == null ? null : gooData8.boo")
.withPrecedence(CONDITIONAL);
// TODO(user): the gencode currently re-evaluates nested null-safe accesses,
// such as opt_data.boo and opt_data.boo[0] below. The correct (and simpler) solution
// is to emit conditional statements, but we can't generate non-expressions outside of
// test code yet.
assertThatSoyExpr("$boo?[0]?[1]")
.generatesCode(
"opt_data.boo == null"
+ " ? null : opt_data.boo[0] == null ? null : opt_data.boo[0][1]")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a?.b.c")
.generatesCode("opt_data.a == null ? null : opt_data.a.b.c")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a?.b[1]")
.generatesCode("opt_data.a == null ? null : opt_data.a.b[1]")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a?[1].b")
.generatesCode("opt_data.a == null ? null : opt_data.a[1].b")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a?[1][2]")
.generatesCode("opt_data.a == null ? null : opt_data.a[1][2]")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a.b?.c")
.generatesCode("opt_data.a.b == null ? null : opt_data.a.b.c")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a.b?[1]")
.generatesCode("opt_data.a.b == null ? null : opt_data.a.b[1]")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a[1]?.b")
.generatesCode("opt_data.a[1] == null ? null : opt_data.a[1].b")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a[1]?[2]")
.generatesCode("opt_data.a[1] == null ? null : opt_data.a[1][2]")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("$a.b?.c.d?.e")
.generatesCode(
"opt_data.a.b == null "
+ "? null : opt_data.a.b.c.d == null ? null : opt_data.a.b.c.d.e")
.withPrecedence(CONDITIONAL);
}
@Test
public void testGlobal() {
assertThatSoyExpr("MOO_2").generatesCode("MOO_2");
assertThatSoyExpr("aaa.BBB").generatesCode("aaa.BBB");
}
@Test
public void testOperators() {
assertThatSoyExpr("not $boo or true and $goo")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("!opt_data.boo || true && gooData8")
.withPrecedence(OR);
assertThatSoyExpr("( (8-4) + (2-1) )").generatesCode("8 - 4 + (2 - 1)").withPrecedence(PLUS);
assertThatSoyExpr("$foo ?: 0")
.generatesCode("($$temp = opt_data.foo) == null ? 0 : $$temp")
.withPrecedence(NULL_COALESCING);
}
@Test
public void testNullCoalescingNested() {
assertThatSoyExpr("$boo ?: -1")
.generatesCode("($$temp = opt_data.boo) == null ? -1 : $$temp")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("$a ?: $b ?: $c")
.generatesCode(
"($$temp = opt_data.a) == null "
+ "? (($$temp = opt_data.b) == null ? opt_data.c : $$temp) : $$temp")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("$a ?: $b ? $c : $d")
.generatesCode(
"($$temp = opt_data.a) == null ? (opt_data.b ? opt_data.c : opt_data.d) : $$temp")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("$a ? $b ?: $c : $d")
.generatesCode(
"opt_data.a ? (($$temp = opt_data.b) == null " + "? opt_data.c : $$temp) : opt_data.d")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("$a ? $b : $c ?: $d")
.generatesCode(
"opt_data.a ? opt_data.b : ($$temp = opt_data.c) == null ? opt_data.d : $$temp")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("($a ?: $b) ?: $c")
.generatesCode(
"($$temp = ($$temp = opt_data.a) == null ? opt_data.b : $$temp) == null "
+ "? opt_data.c : $$temp")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("$a ?: ($b ?: $c)")
.generatesCode(
"($$temp = opt_data.a) == null "
+ "? (($$temp = opt_data.b) == null ? opt_data.c : $$temp) : $$temp")
.withPrecedence(NULL_COALESCING);
assertThatSoyExpr("($a ?: $b) ? $c : $d")
.generatesCode(
"(($$temp = opt_data.a) == null ? opt_data.b : $$temp) " + "? opt_data.c : opt_data.d")
.withPrecedence(NULL_COALESCING);
}
@Test
public void testBuiltinFunctions() {
assertThatSoyExpr("isFirst($goo) ? 1 : 0")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("gooIndex8 == 0 ? 1 : 0")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("not isLast($goo) ? 1 : 0")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("!(gooIndex8 == gooListLen8 - 1) ? 1 : 0")
.withPrecedence(CONDITIONAL);
assertThatSoyExpr("index($goo) + 1")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("gooIndex8 + 1")
.withPrecedence(PLUS);
assertThatSoyExpr("['abc': $goo]")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("{abc: gooData8}");
assertThatSoyExpr("quoteKeysIfJs(['abc': $goo])")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("{'abc': gooData8}");
assertThatSoyExpr("checkNotNull($goo) ? 1 : 0")
.withInitialLocalVarTranslations(LOCAL_VAR_TRANSLATIONS)
.generatesCode("soy.$$checkNotNull(gooData8) ? 1 : 0")
.withPrecedence(CONDITIONAL);
}
@Test
public void testBuiltinFunctions_v1Expression() {
String soyFile =
""
+ "{namespace ns}\n"
+ "{template .foo deprecatedV1=\"true\"}\n"
+ " {v1Expression('$goo.length()')}\n"
+ "{/template}";
String expectedJs =
""
+ "ns.foo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return soydata.VERY_UNSAFE.ordainSanitizedHtml(opt_data.goo.length());\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " ns.foo.soyTemplateName = 'ns.foo';\n"
+ "}\n";
assertThatSoyFile(soyFile)
.withDeclaredSyntaxVersion(SyntaxVersion.V1_0)
.generatesTemplateThat()
.isEqualTo(expectedJs);
}
}