/*
* Copyright 2010 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.soyparse;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.google.common.base.Joiner;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.FixedIdGenerator;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.base.internal.SoyFileSupplier;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.error.FormattingErrorReporter;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.types.SoyTypeRegistry;
import java.io.StringReader;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests that the Soy file and template parsers properly embed source locations. */
@RunWith(JUnit4.class)
public final class SourceLocationTest {
@Test
public void testLocationsInParsedContent() throws Exception {
assertSourceLocations(
Joiner.on('\n')
.join(
"SoyFileSetNode",
" SoyFileNode",
" TemplateBasicNode @ /example/file.soy:2:1",
" RawTextNode @ /example/file.soy:4:2",
" PrintNode @ /example/file.soy:6:3",
" RawTextNode @ /example/file.soy:7:3",
" CallBasicNode @ /example/file.soy:9:3",
" TemplateBasicNode @ /example/file.soy:11:1",
" RawTextNode @ /example/file.soy:12:2",
""),
Joiner.on('\n')
.join(
"{namespace ns}",
"{template .foo autoescape=\"deprecated-noncontextual\"}", // 1
"{@param world : ?}",
" Hello", // 3
" {lb}", // 4
" {print $world}", // 5
" {rb}!", // 6
"", // 7
" {call bar /}", // 8
"{/template}", // 9
"{template .bar autoescape=\"deprecated-noncontextual\"}", // 10
" Gooodbye", // 11
"{/template}" // 12
));
}
@Test
public void testSwitches() throws Exception {
assertSourceLocations(
Joiner.on('\n')
.join(
"SoyFileSetNode",
" SoyFileNode",
" TemplateBasicNode @ /example/file.soy:2:1",
" RawTextNode @ /example/file.soy:4:2",
" SwitchNode @ /example/file.soy:5:3",
" SwitchCaseNode @ /example/file.soy:6:5",
" RawTextNode @ /example/file.soy:7:7",
" SwitchCaseNode @ /example/file.soy:8:5",
" RawTextNode @ /example/file.soy:9:7",
" SwitchCaseNode @ /example/file.soy:10:5",
" RawTextNode @ /example/file.soy:11:7",
" SwitchDefaultNode @ /example/file.soy:12:5",
" RawTextNode @ /example/file.soy:13:7",
" RawTextNode @ /example/file.soy:15:3",
""),
Joiner.on('\n')
.join(
"{namespace ns}",
"{template .foo autoescape=\"deprecated-noncontextual\"}", // 1
"{@param i : int}", // 2
" Hello,", // 3
" {switch $i}", // 4
" {case 0}", // 5
" Mercury", // 6
" {case 1}", // 7
" Venus", // 8
" {case 2}", // 9
" Mars", // 10
" {default}", // 11
" Gassy", // 12
" {/switch}", // 13
" !", // 14
"{/template}", // 15
""));
}
@Test
public void testForLoop() throws Exception {
assertSourceLocations(
Joiner.on('\n')
.join(
"SoyFileSetNode",
" SoyFileNode",
" TemplateBasicNode @ /example/file.soy:2:1",
" RawTextNode @ /example/file.soy:3:2",
" ForNode @ /example/file.soy:4:3",
" RawTextNode @ /example/file.soy:5:5",
" PrintNode @ /example/file.soy:6:5",
" RawTextNode @ /example/file.soy:8:3",
""),
Joiner.on('\n')
.join(
"{namespace ns}",
"{template .foo autoescape=\"deprecated-noncontextual\"}", // 1
" Hello", // 2
" {for $i in range(0, 1, 10)}", // 3
" ,", // 4
" {print $i}", // 5
" {/for}", // 6
" !", // 7
"{/template}", // 8
""));
}
@Test
public void testForeachLoop() throws Exception {
assertSourceLocations(
Joiner.on('\n')
.join(
"SoyFileSetNode",
" SoyFileNode",
" TemplateBasicNode @ /example/file.soy:2:1",
" RawTextNode @ /example/file.soy:3:2",
" ForeachNode @ /example/file.soy:4:3",
" ForeachNonemptyNode @ /example/file.soy:4:3",
" RawTextNode @ /example/file.soy:5:5",
" PrintNode @ /example/file.soy:6:5",
" ForeachIfemptyNode @ /example/file.soy:7:3",
" RawTextNode @ /example/file.soy:8:5",
" RawTextNode @ /example/file.soy:10:3",
""),
Joiner.on('\n')
.join(
"{namespace ns}",
"{template .foo autoescape=\"deprecated-noncontextual\"}", // 1
" Hello", // 2
" {foreach $planet in ['mercury', 'mars', 'venus']}", // 3
" ,", // 4
" {print $planet}", // 5
" {ifempty}", // 6
" lifeless interstellar void", // 7
" {/foreach}", // 8
" !", // 9
"{/template}", // 10
""));
}
@Test
public void testConditional() throws Exception {
assertSourceLocations(
Joiner.on('\n')
.join(
"SoyFileSetNode",
" SoyFileNode",
" TemplateBasicNode @ /example/file.soy:2:1",
" RawTextNode @ /example/file.soy:5:2",
" IfNode @ /example/file.soy:6:3",
" IfCondNode @ /example/file.soy:6:3",
" RawTextNode @ /example/file.soy:7:5",
" IfCondNode @ /example/file.soy:8:3",
" RawTextNode @ /example/file.soy:9:5",
" IfElseNode @ /example/file.soy:10:3",
" RawTextNode @ /example/file.soy:11:5",
" RawTextNode @ /example/file.soy:13:3",
""),
Joiner.on('\n')
.join(
"{namespace ns}",
"{template .foo autoescape=\"deprecated-noncontextual\"}", // 1
"{@param skyIsBlue : bool}",
"{@param isReallyReallyHot : bool}",
" Hello,", // 4
" {if $skyIsBlue}", // 5
" Earth", // 6
" {elseif $isReallyReallyHot}", // 7
" Venus", // 8
" {else}", // 9
" Cincinatti", // 10
" {/if}", // 11
" !", // 12
"{/template}", // 13
""));
}
@Test
public void testDoesntAccessPastEnd() {
// Make sure that if we have a token stream that ends abruptly, we don't
// look for a line number and break in a way that suppresses the real error
// message.
// JavaCC is pretty good about never using null as a token value.
FormattingErrorReporter reporter = new FormattingErrorReporter();
SoyFileSetParserBuilder.forSuppliers(
SoyFileSupplier.Factory.create(
"{template t autoescape=\"deprecated-noncontextual\"}\nHello, World!\n",
SoyFileKind.SRC,
"borken.soy"))
.errorReporter(reporter)
.parse();
assertThat(reporter.getErrorMessages()).isNotEmpty();
}
@Test
public void testAdditionalSourceLocationInfo() throws Exception {
String template = "{namespace ns}\n" + "{template .t}\n" + " hello, world\n" + "{/template}\n";
TemplateNode templateNode =
new SoyFileParser(
new SoyTypeRegistry(),
new FixedIdGenerator(),
new StringReader(template),
SoyFileKind.SRC,
"/example/file.soy",
ExplodingErrorReporter.get())
.parseSoyFile()
.getChild(0);
SourceLocation location = templateNode.getSourceLocation();
// Begin at {template
assertEquals(2, location.getBeginLine());
assertEquals(1, location.getBeginColumn());
// End after .t}
assertEquals(2, location.getEndLine());
assertEquals(13, location.getEndColumn());
}
@Test
public void testRawTextSourceLocations() throws Exception {
// RawTextNode has some special methods to calculating the source location of characters within
// the strings, test those
String template =
Joiner.on('\n')
.join(
"{namespace ns}",
"{template .foo}",
" Hello,{sp}",
" {\\n}{nil}<span>Bob</span>",
" // and end of line comment",
" !",
" What's /*hello comment world*/up?",
"{/template}",
"");
RawTextNode rawText =
(RawTextNode)
new SoyFileParser(
new SoyTypeRegistry(),
new FixedIdGenerator(),
new StringReader(template),
SoyFileKind.SRC,
"/example/file.soy",
ExplodingErrorReporter.get())
.parseSoyFile()
.getChild(0)
.getChild(1);
assertThat(rawText.getRawText()).isEqualTo("Hello, \n<span>Bob</span>! What's up?");
assertThat(rawText.getRawText().substring(0, 5)).isEqualTo("Hello");
SourceLocation loc = rawText.substringLocation(0, 5);
assertThat(loc.getBeginLine()).isEqualTo(3);
assertThat(loc.getBeginColumn()).isEqualTo(3);
assertThat(loc.getEndLine()).isEqualTo(3);
assertThat(loc.getEndColumn()).isEqualTo(7);
assertThat(rawText.getRawText().substring(8, 14)).isEqualTo("<span>");
loc = rawText.substringLocation(8, 14);
assertThat(loc.getBeginLine()).isEqualTo(4);
assertThat(loc.getBeginColumn()).isEqualTo(12);
assertThat(loc.getEndLine()).isEqualTo(4);
assertThat(loc.getEndColumn()).isEqualTo(17);
assertThat(rawText.getRawText().substring(24, 25)).isEqualTo("!");
loc = rawText.substringLocation(24, 25);
assertThat(loc.getBeginLine()).isEqualTo(6);
assertThat(loc.getBeginColumn()).isEqualTo(3);
assertThat(loc.getEndLine()).isEqualTo(6);
assertThat(loc.getEndColumn()).isEqualTo(3);
assertThat(rawText.getRawText().substring(33, 36)).isEqualTo("up?");
loc = rawText.substringLocation(33, 36);
assertThat(loc.getBeginLine()).isEqualTo(7);
assertThat(loc.getBeginColumn()).isEqualTo(33);
assertThat(loc.getEndLine()).isEqualTo(7);
assertThat(loc.getEndColumn()).isEqualTo(35);
final int id = 1337; // doesn't matter
RawTextNode subStringNode = rawText.substring(id, 0, 5);
assertThat(subStringNode.getRawText()).isEqualTo("Hello");
loc = subStringNode.getSourceLocation();
assertThat(loc.getBeginLine()).isEqualTo(3);
assertThat(loc.getBeginColumn()).isEqualTo(3);
assertThat(loc.getEndLine()).isEqualTo(3);
assertThat(loc.getEndColumn()).isEqualTo(7);
subStringNode = rawText.substring(id, 24, 25);
assertThat(subStringNode.getRawText()).isEqualTo("!");
loc = subStringNode.getSourceLocation();
assertThat(loc.getBeginLine()).isEqualTo(6);
assertThat(loc.getBeginColumn()).isEqualTo(3);
assertThat(loc.getEndLine()).isEqualTo(6);
assertThat(loc.getEndColumn()).isEqualTo(3);
// Can't create empty raw text nodes.
try {
rawText.substring(id, 24, 24);
fail();
} catch (IllegalArgumentException expected) {
}
try {
rawText.substring(id, 24, 23);
fail();
} catch (IllegalArgumentException expected) {
}
try {
rawText.substring(id, 24, Integer.MAX_VALUE);
fail();
} catch (IllegalArgumentException expected) {
}
}
private void assertSourceLocations(String asciiArtExpectedOutput, String soySourceCode) {
SoyFileSetNode soyTree =
SoyFileSetParserBuilder.forSuppliers(
SoyFileSupplier.Factory.create(soySourceCode, SoyFileKind.SRC, "/example/file.soy"))
.parse()
.fileSet();
String actual = new AsciiArtVisitor().exec(soyTree);
assertEquals(
// Make the message be something copy-pasteable to make it easier to update this test when
// fixing source locations bugs.
"REPLACE_WITH:\n\"" + actual.replaceAll("\n", "\",\n\"") + "\"\n\n",
asciiArtExpectedOutput,
actual);
}
/** Generates a concise readable summary of a soy tree and its source locations. */
private static class AsciiArtVisitor extends AbstractSoyNodeVisitor<String> {
final StringBuilder sb = new StringBuilder();
int depth;
@Override
public String exec(SoyNode node) {
visit(node);
return sb.toString();
}
@Override
protected void visitSoyNode(SoyNode node) {
// Output a header like:
// <indent> <node class> @ <location>
// where indent is 2 spaces per level, and the @ sign is indented to the 31st column.
for (int indent = depth; --indent >= 0; ) {
sb.append(" ");
}
String typeName = node.getClass().getSimpleName();
sb.append(typeName);
// SoyFileSetNode and SoyFileNode don't have source locations.
if (!(node instanceof SoyFileSetNode) && !(node instanceof SoyFileNode)) {
int pos = typeName.length() + 2 * depth;
while (pos < 30) {
sb.append(' ');
++pos;
}
sb.append(" @ ").append(node.getSourceLocation());
}
sb.append('\n');
if (node instanceof ParentSoyNode<?>) {
++depth;
visitChildren((ParentSoyNode<?>) node);
--depth;
}
}
}
}