/*
* Copyright 2013 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.passes;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.basetree.SyntaxVersion;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.error.FormattingErrorReporter;
import com.google.template.soy.exprtree.VarDefn;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.shared.SoyGeneralOptions;
import com.google.template.soy.soytree.ForeachNode;
import com.google.template.soy.soytree.ForeachNonemptyNode;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.LetValueNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyTypeProvider;
import com.google.template.soy.types.SoyTypeRegistry;
import com.google.template.soy.types.primitive.UnknownType;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for ResolveNamesVisitor.
*
*/
@RunWith(JUnit4.class)
public final class ResolveNamesVisitorTest {
private static final SoyTypeProvider typeProvider =
new SoyTypeProvider() {
@Override
public SoyType getType(String typeName, SoyTypeRegistry typeRegistry) {
if (typeName.equals("unknown")) {
return UnknownType.getInstance();
}
return null;
}
};
private static final SoyTypeRegistry typeRegistry =
new SoyTypeRegistry(ImmutableSet.of(typeProvider));
@Test
public void testParamNameLookupSuccess() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
constructTemplateSource("{@param pa: bool}", "{$pa ? 1 : 0}"))
.parse()
.fileSet();
new ResolveNamesVisitor(ExplodingErrorReporter.get()).exec(soyTree);
TemplateNode n = soyTree.getChild(0).getChild(0);
assertThat(n.getMaxLocalVariableTableSize()).isEqualTo(1);
assertThat(n.getParams().get(0).localVariableIndex()).isEqualTo(0);
}
@Test
public void testInjectedParamNameLookupSuccess() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
constructTemplateSource("{@inject pa: bool}", "{$pa ? 1 : 0}"))
.parse()
.fileSet();
new ResolveNamesVisitor(ExplodingErrorReporter.get()).exec(soyTree);
TemplateNode n = soyTree.getChild(0).getChild(0);
assertThat(n.getMaxLocalVariableTableSize()).isEqualTo(1);
assertThat(n.getInjectedParams().get(0).localVariableIndex()).isEqualTo(0);
}
@Test
public void testLetNameLookupSuccess() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(constructTemplateSource("{let $pa: 1 /}", "{$pa}"))
.parse()
.fileSet();
new ResolveNamesVisitor(ExplodingErrorReporter.get()).exec(soyTree);
TemplateNode n = soyTree.getChild(0).getChild(0);
assertThat(n.getMaxLocalVariableTableSize()).isEqualTo(1);
assertThat(((LetValueNode) n.getChild(0)).getVar().localVariableIndex()).isEqualTo(0);
}
@Test
public void testMultipleLocalsAndScopesNumbering() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
constructTemplateSource(
"{@param pa: bool}",
"{@param pb: bool}",
"{let $la: 1 /}",
"{foreach $item in ['a', 'b']}",
" {$pa ? 1 : 0}{$pb ? 1 : 0}{$la + $item}",
"{/foreach}",
"{let $lb: 1 /}"))
.parse()
.fileSet();
new ResolveNamesVisitor(ExplodingErrorReporter.get()).exec(soyTree);
TemplateNode n = soyTree.getChild(0).getChild(0);
// 6 because we have 2 params, 1 let and a foreach loop var which needs 3 slots (variable,
// index, lastIndex) active within the foreach loop. the $lb can reuse a slot for the foreach
// loop variable
assertThat(n.getMaxLocalVariableTableSize()).isEqualTo(6);
assertThat(n.getParams().get(0).localVariableIndex()).isEqualTo(0);
assertThat(n.getParams().get(1).localVariableIndex()).isEqualTo(1);
assertThat(((LetValueNode) n.getChild(0)).getVar().localVariableIndex()).isEqualTo(2);
ForeachNonemptyNode foreachNonEmptyNode =
(ForeachNonemptyNode) ((ForeachNode) n.getChild(1)).getChild(0);
assertThat(foreachNonEmptyNode.getVar().localVariableIndex()).isEqualTo(3);
assertThat(foreachNonEmptyNode.getVar().currentLoopIndexIndex()).isEqualTo(4);
assertThat(foreachNonEmptyNode.getVar().isLastIteratorIndex()).isEqualTo(5);
// The loop variables are out of scope so we can reuse the 3rd slot
assertThat(((LetValueNode) n.getChild(2)).getVar().localVariableIndex()).isEqualTo(3);
}
@Test
public void testMultipleLocals() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
constructTemplateSource("{let $la: 1 /}", "{let $lb: $la /}", "{let $lc: $lb /}"))
.parse()
.fileSet();
new ResolveNamesVisitor(ExplodingErrorReporter.get()).exec(soyTree);
TemplateNode n = soyTree.getChild(0).getChild(0);
// 3 because each new $la binding is a 'new variable'
assertThat(n.getMaxLocalVariableTableSize()).isEqualTo(3);
LetValueNode firstLet = (LetValueNode) n.getChild(0);
LetValueNode secondLet = (LetValueNode) n.getChild(1);
LetValueNode thirdLet = (LetValueNode) n.getChild(2);
assertThat(firstLet.getVar().localVariableIndex()).isEqualTo(0);
assertThat(secondLet.getVar().localVariableIndex()).isEqualTo(1);
assertThat(thirdLet.getVar().localVariableIndex()).isEqualTo(2);
assertThat(((VarRefNode) secondLet.getValueExpr().getRoot()).getDefnDecl())
.isEqualTo(firstLet.getVar());
assertThat(((VarRefNode) thirdLet.getValueExpr().getRoot()).getDefnDecl())
.isEqualTo(secondLet.getVar());
}
@Test
public void testVariableNameRedefinition() {
assertResolveNamesFails(
"variable '$la' already defined at line 4",
constructTemplateSource("{let $la: 1 /}", "{let $la: $la /}"));
assertResolveNamesFails(
"variable '$pa' already defined",
constructTemplateSource("{@param pa: bool}", "{let $pa: not $pa /}"));
assertResolveNamesFails(
"variable '$la' already defined at line 4",
constructTemplateSource(
"{let $la: 1 /}", "{foreach $item in ['a', 'b']}", " {let $la: $la /}", "{/foreach}"));
assertResolveNamesFails(
"variable '$group' already defined",
constructTemplateSource(
"{@param group: string}",
"{foreach $group in ['a', 'b']}",
" {$group}",
"{/foreach}"));
// valid, $item and $la are defined in non-overlapping scopes
SoyFileSetParserBuilder.forFileContents(
constructTemplateSource(
"{foreach $item in ['a', 'b']}",
" {let $la: 1 /}",
"{/foreach}",
"{foreach $item in ['a', 'b']}",
" {let $la: 1 /}",
"{/foreach}"))
.parse()
.fileSet();
}
@Test
public void testAccidentalGlobalReference() {
assertResolveNamesFails(
"Found global reference aliasing a local variable 'group', did you mean '$group'?",
constructTemplateSource("{@param group: string}", "{if group}{$group}{/if}"));
assertResolveNamesFails(
"Found global reference aliasing a local variable 'group', did you mean '$group'?",
constructTemplateSource("{let $group: 'foo' /}", "{if group}{$group}{/if}"));
assertResolveNamesFails(
"Unbound global 'global'.",
constructTemplateSource("{let $local: 'foo' /}", "{if global}{$local}{/if}"));
}
@Test
public void testLetContentSlotLifetime() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(
constructTemplateSource(
"{let $a}",
" {if true}", // introduce an extra scope
" {let $b: 2 /}",
" {$b}",
" {/if}",
"{/let}",
"{$a}"))
.parse()
.fileSet();
new ResolveNamesVisitor(ExplodingErrorReporter.get()).exec(soyTree);
TemplateNode n = soyTree.getChild(0).getChild(0);
// 1 because each new $la binding overwrites the prior one
assertThat(n.getMaxLocalVariableTableSize()).isEqualTo(2);
LetContentNode aLetNode = (LetContentNode) n.getChild(0);
assertThat(aLetNode.getVar().localVariableIndex()).isEqualTo(1);
LetValueNode bLetNode =
(LetValueNode) ((IfCondNode) ((IfNode) aLetNode.getChild(0)).getChild(0)).getChild(0);
assertThat(bLetNode.getVar().localVariableIndex()).isEqualTo(0);
}
@Test
public void testLetReferencedInsideAttributeValue() {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forFileContents(constructTemplateSource("{let $t: 1 /}<{$t}>"))
.options(
new SoyGeneralOptions().setExperimentalFeatures(ImmutableList.of("stricthtml")))
.parse()
.fileSet();
TemplateNode n = soyTree.getChild(0).getChild(0);
VarRefNode node = Iterables.getOnlyElement(SoyTreeUtils.getAllNodesOfType(n, VarRefNode.class));
assertThat(node.getDefnDecl().kind()).isEqualTo(VarDefn.Kind.LOCAL_VAR);
}
@Test
@Ignore
public void testNameLookupFailure() {
// This fails currently because we aren't setting SyntaxVersion.V9_9
// But referencing unknown variables is actually currently handled by the
// CheckTemplateParamsVisitor. So this test is potentially silly anyway. Consider
// 1. removing this dead feature
// 2. moving this functionality from CheckTemplateParamsVisitor to ResolveNamesVisitor where it
// belongs
// http://b/21877289 covers various issues with syntax version
assertResolveNamesFails("Undefined variable", constructTemplateSource("{$pa}"));
}
/**
* Helper function that constructs a boilerplate template given a list of body statements to
* insert into the middle of the template. The body statements will be indented and separated with
* newlines.
*
* @param body The body statements.
* @return The combined template.
*/
private static String constructTemplateSource(String... body) {
return ""
+ "{namespace ns}\n"
+ "/***/\n"
+ "{template .aaa}\n"
+ " "
+ Joiner.on("\n ").join(body)
+ "\n"
+ "{/template}\n";
}
private void assertResolveNamesFails(String expectedError, String fileContent) {
FormattingErrorReporter errorReporter = new FormattingErrorReporter();
SoyFileSetParserBuilder.forFileContents(fileContent)
.declaredSyntaxVersion(SyntaxVersion.V2_0)
.errorReporter(errorReporter)
.typeRegistry(typeRegistry)
.parse();
assertThat(errorReporter.getErrorMessages()).hasSize(1);
assertThat(errorReporter.getErrorMessages().get(0)).isEqualTo(expectedError);
}
}