/*
* Copyright 2016 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 org.junit.Assert.fail;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.exprtree.DataAccessNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.TemplateNode;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link TemplateAnalysis}. */
@RunWith(JUnit4.class)
public final class TemplateAnalysisTest {
// All the tests are written as soy templates with references to two soy functions 'refed' and
// 'notrefed'.
// if a variable is passed to 'refed' then it means that at that point in the program we expect
// that it has definitely already been referenced.
// 'notrefed' just means the opposite. at that point in the program there is no guarantee that
// it has already been referenced.
@Test
public void testSimpleSequentalAccess() {
runTest("{@param p : string}", "{notrefed($p)}{refed($p)}");
runTest(
"{@param b : bool}",
"{@param p1 : string}",
"{@param p2 : string}",
"{@param p3 : string}",
"{$b ? $p1 + $p3 : $p2 + $p3}",
"{refed($b)}",
"{notrefed($p1)}",
"{notrefed($p2)}",
"{refed($p3)}");
}
@Test
public void testDataAccess() {
runTest("{@param p : list<string>}", "{notrefed($p[0])}", "{refed($p[0])}");
runTest("{@param p : [field:string]}", "{notrefed($p.field)}", "{refed($p.field)}");
}
@Test
public void testIf() {
// conditions are refed prior to the blocks they control
// if there is an {else} then anything refed in all branches is refed after the if
runTest(
"{@param p1 : string}",
"{@param p2 : string}",
"{@param p3 : string}",
"{if $p1}",
" {refed($p1)}",
" {notrefed($p2)}",
" {$p3}",
"{elseif $p2}",
" {refed($p1)}",
" {refed($p2)}",
" {$p3}",
"{else}",
" {refed($p1)}",
" {refed($p2)}",
" {$p3}",
"{/if}",
"{refed($p3)}");
runTest(
"{@param p : string}",
"{@param b1 : bool}",
"{@param b2 : bool}",
"{if $b1}",
" {$p}",
" {refed($b1)}",
" {notrefed($b2)}",
"{elseif $b2}",
" {$p}",
" {refed($b1)}",
" {refed($b2)}",
"{/if}",
"{notrefed($p)}");
}
@Test
public void testSwitch() {
// empty switch
runTest("{@param p : int}", "{switch $p}", "{/switch}", "{notrefed($p)}");
// only default, switch expression still not evaluated
runTest(
"{@param p : int}",
"{@param p2 : int}",
"{switch $p}",
"{default}",
" {$p2}",
"{/switch}",
"{notrefed($p)}",
"{refed($p2)}");
// cases
runTest(
"{@param p : int}",
"{@param p2 : int}",
"{switch $p}",
"{case $p2}",
" {refed($p)}",
" {refed($p2)}",
"{default}",
" {$p2}",
"{/switch}",
"{refed($p)}",
"{refed($p2)}");
// cases
runTest(
"{@param p : int}",
"{@param p2 : int}",
"{@param p3 : int}",
"{switch $p}",
"{case $p2}",
" {refed($p)}",
" {refed($p2)}",
"{case $p3}",
" {refed($p)}",
" {refed($p2)}",
" {refed($p3)}",
"{/switch}",
"{refed($p)}",
"{refed($p2)}",
"{notrefed($p3)}"); // p3 is not refed because it only happens if $p != $p2
}
@Test
public void testFor() {
runTest(
"{@param limit : int}",
"{@param p : string}",
"{for $i in range(0, $limit)}",
" {$p}",
" {$i}",
" {refed($limit)}",
"{/for}",
"{refed($limit)}",
"{notrefed($p)}");
// In this case we can prove that the loop will execute and thus p will have been referenced
// after the loop.
runTest(
"{@param p : string}",
"{for $i in range(0, 1)}",
" {$p}",
" {$i}",
"{/for}",
"{refed($p)}");
}
@Test
public void testForeach() {
// test special functions for foreach loops. though these all look like references to the loop
// var, they actually aren't.
runTest(
"{@param list : list<?>}",
"{foreach $item in $list}",
" {if isFirst($item)}first{/if}",
" {if isLast($item)}last{/if}",
" {index($item)}",
" {notrefed($item)}",
" {refed($list)}",
"{/foreach}",
"{refed($list)}");
// test ifempty blocks
runTest(
"{@param list : list<?>}",
"{@param p: ?}",
"{@param p2: ?}",
"{@param p3: ?}",
"{foreach $item in $list}",
" {$p}",
" {$p2}",
"{ifempty}",
" {$p}",
" {$p3}",
"{/foreach}",
"{refed($list)}",
"{refed($p)}",
"{notrefed($p2)}",
"{notrefed($p3)}");
}
@Test
public void testForeach_literalList() {
// test literal lists
// empty list
runTest(
"{call .loop data=\"all\"}",
" {param list: [] /}",
"{/call}",
"{/template}",
"",
"{template .loop}",
"{@param list: list<?>}",
"{@param p: ?}",
"{@param p2: ?}",
"{foreach $item in $list}",
" {$p}",
"{ifempty}",
" {$p2}",
"{/foreach}",
"{notrefed($p)}",
"{refed($p2)}");
// nonempty list
runTest(
"{@param p: ?}",
"{@param p2: ?}",
"{foreach $item in [1, 2, 3]}",
" {$p}",
"{ifempty}",
" {$p2}",
"{/foreach}",
"{refed($p)}",
"{notrefed($p2)}");
}
@Test
public void testLetVariable() {
runTest("{@param p: ?}", "{let $l : $p/}", "{notrefed($p)}");
// referencing $l implies we have refed $p
runTest("{@param p: ?}", "{let $l : $p/}", "{$l}", "{refed($p)}");
runTest(
"{@param p: ?}",
"{let $l kind=\"text\"}{$p}{/let}",
"{let $l2 : '' + $l /}",
"{notrefed($l2)}",
"{refed($l2)}",
"{refed($l)}",
"{refed($p)}");
}
@Test
public void testRefsInLets() {
runTest(
"{@param p: ?}",
"{let $l kind=\"text\"}{$p}{/let}",
"{let $l2 : notrefed($l) ? refed($l) : '' /}",
"{notrefed($p)}",
"{notrefed($l2)}",
"{refed($l2)}",
"{refed($l)}");
}
@Test
public void testMsg() {
runTest("{@param p : ?}", "{msg desc=\"\"}", " Hello {$p}", "{/msg}", "{refed($p)}");
runTest(
"{@param p : ?}",
"{msg desc=\"\"}",
" Hello {$p}",
"{fallbackmsg desc=\"\"}",
" Hello foo",
"{/msg}",
"{notrefed($p)}");
runTest(
"{@param p : ?}",
"{msg desc=\"\"}",
" Hello {$p}",
"{fallbackmsg desc=\"\"}",
" Hello old {$p}",
"{/msg}",
"{refed($p)}");
}
@Test
public void testCall() {
// The tricky thing about calls is how params are handled
runTest("{@param p : ?}", "{call .foo data=\"$p\"/}", "{refed($p)}");
runTest("{@param p : ?}", "{call .foo data=\"all\"/}", "{notrefed($p)}");
runTest(
"{@param p : ?}",
"{call .foo}",
" {param p1 : notrefed($p) /}",
" {param p2 : notrefed($p) /}",
"{/call}",
"{notrefed($p)}");
runTest(
"{@param p : ?}",
"{$p}",
"{call .foo}",
" {param p1 : refed($p) /}",
" {param p2 : refed($p) /}",
"{/call}",
"{refed($p)}");
}
void runTest(String... lines) {
TemplateNode template = parseTemplate(lines);
TemplateAnalysis analysis = TemplateAnalysis.analyze(template);
for (FunctionNode node : SoyTreeUtils.getAllNodesOfType(template, FunctionNode.class)) {
if (node.getSoyFunction() == NOT_REFED_FUNCTION) {
checkNotReferenced(analysis, node.getChild(0));
} else if (node.getSoyFunction() == REFED_FUNCTION) {
checkReferenced(analysis, node.getChild(0));
}
}
}
private void checkNotReferenced(TemplateAnalysis analysis, ExprNode child) {
if (hasDefinitelyAlreadyBeenAccessed(analysis, child)) {
fail("Expected reference to " + format(child) + " to have not been definitely referenced.");
}
}
private void checkReferenced(TemplateAnalysis analysis, ExprNode child) {
if (!hasDefinitelyAlreadyBeenAccessed(analysis, child)) {
fail("Expected reference to " + format(child) + " to have been definitely referenced.");
}
}
private boolean hasDefinitelyAlreadyBeenAccessed(TemplateAnalysis analysis, ExprNode child) {
if (child instanceof VarRefNode) {
return analysis.isResolved((VarRefNode) child);
}
if (child instanceof DataAccessNode) {
return analysis.isResolved((DataAccessNode) child);
}
return false;
}
private String format(ExprNode child) {
SourceLocation sourceLocation = child.getSourceLocation();
// subtract 2 from the line number since the boilerplate adds 2 lines above the user content
return child.toSourceString()
+ " at "
+ (sourceLocation.getBeginLine() - 2)
+ ":"
+ sourceLocation.getBeginColumn();
}
private static TemplateNode parseTemplate(String... lines) {
return SoyFileSetParserBuilder.forFileContents(
Joiner.on("\n")
.join(
"{namespace test}",
"{template .caller}",
Joiner.on("\n").join(lines),
"{/template}",
"",
// add an additional template as a callee.
"{template .foo}",
" {@param? p1 : ?}",
" {@param? p2 : ?}",
" {$p1 + $p2}",
"{/template}",
""))
.addSoyFunction(REFED_FUNCTION)
.addSoyFunction(NOT_REFED_FUNCTION)
.parse()
.fileSet()
.getChild(0)
.getChild(0);
}
private static final SoyFunction NOT_REFED_FUNCTION =
new SoyFunction() {
@Override
public String getName() {
return "notrefed";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(1);
}
};
private static final SoyFunction REFED_FUNCTION =
new SoyFunction() {
@Override
public String getName() {
return "refed";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(1);
}
};
}