From 22bd4aecd8b9fa2ea61fbd2736e54c75d4e32b26 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 8 Jan 2020 17:17:27 +0100 Subject: [PATCH] Qute parser - improve error messages - resolves #6164 --- .../qute/deployment/QuteProcessor.java | 37 +++++- .../qute/deployment/TemplateException.java | 11 -- .../deployment/NamedBeanNotFoundTest.java | 1 + .../NamedBeanPropertyNotFoundTest.java | 1 + .../qute/deployment/PropertyNotFoundTest.java | 1 + .../deployment/TypeSafeLoopFailureTest.java | 1 + .../main/java/io/quarkus/qute/EngineImpl.java | 5 +- .../src/main/java/io/quarkus/qute/Parser.java | 107 ++++++++++++------ .../java/io/quarkus/qute/SectionNode.java | 1 + .../io/quarkus/qute/TemplateException.java | 32 ++++++ .../java/io/quarkus/qute/TemplateLocator.java | 2 +- .../java/io/quarkus/qute/TemplateNode.java | 4 + .../test/java/io/quarkus/qute/ParserTest.java | 84 ++++++++++---- 13 files changed, 212 insertions(+), 75 deletions(-) delete mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateException.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateException.java diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 27989177efa35..e3a6ee9296fc5 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -5,6 +5,8 @@ import java.io.File; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -71,7 +73,10 @@ import io.quarkus.qute.SectionHelper; import io.quarkus.qute.SectionHelperFactory; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.TemplateLocator; +import io.quarkus.qute.Variant; import io.quarkus.qute.api.ResourcePath; import io.quarkus.qute.api.VariantTemplate; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; @@ -163,15 +168,37 @@ public CompletionStage resolve(SectionResolutionContext context) { }; } }; + }).addLocator(new TemplateLocator() { + @Override + public Optional locate(String id) { + TemplatePathBuildItem found = templatePaths.stream().filter(p -> p.getPath().equals(id)).findAny().orElse(null); + if (found != null) { + try { + byte[] content = Files.readAllBytes(found.getFullPath()); + return Optional.of(new TemplateLocation() { + @Override + public Reader read() { + return new StringReader(new String(content, StandardCharsets.UTF_8)); + } + + @Override + public Optional getVariant() { + return Optional.empty(); + } + }); + } catch (IOException e) { + LOGGER.warn("Unable to read the template from path: " + found.getFullPath(), e); + } + } + ; + return Optional.empty(); + } }).build(); for (TemplatePathBuildItem path : templatePaths) { - try { - Template template = dummyEngine - .parse(new String(Files.readAllBytes(path.getFullPath()), StandardCharsets.UTF_8)); + Template template = dummyEngine.getTemplate(path.getPath()); + if (template != null) { analysis.add(new TemplateAnalysis(template.getGeneratedId(), template.getExpressions(), path)); - } catch (IOException e) { - LOGGER.warn("Unable to analyze the template from path: " + path.getFullPath(), e); } } LOGGER.debugf("Finished analysis of %s templates in %s ms", diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateException.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateException.java deleted file mode 100644 index 6d9f423622f20..0000000000000 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateException.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.qute.deployment; - -public class TemplateException extends RuntimeException { - - private static final long serialVersionUID = 1336799943548973690L; - - public TemplateException(String message) { - super(message); - } - -} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java index 858fadfd51c2d..fb1b765cc1b42 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanNotFoundTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.qute.TemplateException; import io.quarkus.test.QuarkusUnitTest; public class NamedBeanNotFoundTest { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java index 71e9a20ec2bf6..0ff680f76a8b6 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/NamedBeanPropertyNotFoundTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.qute.TemplateException; import io.quarkus.test.QuarkusUnitTest; public class NamedBeanPropertyNotFoundTest { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java index 950d05283c147..ecb2a3a74d60a 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.qute.TemplateException; import io.quarkus.test.QuarkusUnitTest; public class PropertyNotFoundTest { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java index 92e2062de7ca4..0fb45a18d5561 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeSafeLoopFailureTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.qute.TemplateException; import io.quarkus.test.QuarkusUnitTest; public class TypeSafeLoopFailureTest { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java index 191e474939bec..385c0b097dfc1 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java @@ -68,7 +68,8 @@ class EngineImpl implements Engine { @Override public Template parse(String content, Variant variant) { - return new Parser(this).parse(new StringReader(content), Optional.ofNullable(variant)); + String generatedId = generateId(); + return new Parser(this).parse(new StringReader(content), Optional.ofNullable(variant), generatedId, generatedId); } @Override @@ -131,7 +132,7 @@ private Template load(String id) { Optional location = locator.locate(id); if (location.isPresent()) { try (Reader r = location.get().read()) { - return new Parser(this).parse(ensureBufferedReader(r), location.get().getVariant()); + return new Parser(this).parse(ensureBufferedReader(r), location.get().getVariant(), id, generateId()); } catch (IOException e) { LOGGER.warn("Unable to close the reader for " + id, e); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index 311e3aaa6243f..901722bde5e9e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -41,13 +41,15 @@ class Parser implements Function { private StringBuilder buffer; private State state; private int line; + private int lineCharacter; private final Deque sectionStack; private final Deque sectionBlockStack; private final Deque paramsStack; private final Deque> typeInfoStack; private int sectionBlockIdx; private boolean ignoreContent; - private String templateId; + private String id; + private String generatedId; private Optional variant; public Parser(EngineImpl engine) { @@ -56,7 +58,8 @@ public Parser(EngineImpl engine) { this.buffer = new StringBuilder(); this.sectionStack = new ArrayDeque<>(); this.sectionStack - .addFirst(SectionNode.builder(ROOT_HELPER_NAME, new OriginImpl(line, templateId, variant)).setEngine(engine) + .addFirst(SectionNode.builder(ROOT_HELPER_NAME, origin()) + .setEngine(engine) .setHelperFactory(new SectionHelperFactory() { @Override public SectionHelper initialize(SectionInitContext context) { @@ -78,11 +81,13 @@ public CompletionStage resolve(SectionResolutionContext context) { this.typeInfoStack = new ArrayDeque<>(); this.typeInfoStack.addFirst(new HashMap<>()); this.line = 1; + this.lineCharacter = 1; } - Template parse(Reader reader, Optional variant) { + Template parse(Reader reader, Optional variant, String id, String generatedId) { long start = System.currentTimeMillis(); - this.templateId = engine.generateId(); + this.id = id; + this.generatedId = generatedId; this.variant = variant; try { int val; @@ -95,30 +100,29 @@ Template parse(Reader reader, Optional variant) { // Flush the last text segment flushText(); } else { - throw new IllegalStateException( - "Unexpected non-text buffer at the end of the document (probably unterminated tag):" + - buffer); + parserError( + "unexpected non-text buffer at the end of the template - probably an unterminated tag: " + buffer); } } SectionNode.Builder root = sectionStack.peek(); if (root == null) { - throw new IllegalStateException("No root section found!"); + parserError("no root section found"); } if (!root.helperName.equals(ROOT_HELPER_NAME)) { - throw new IllegalStateException("The last section on the stack is not a root but: " + root.helperName); + parserError("unterminated section [" + root.helperName + "] detected"); } SectionBlock.Builder part = sectionBlockStack.peek(); if (part == null) { - throw new IllegalStateException("No root section part found!"); + parserError("no root section part found"); } root.addBlock(part.build()); - Template template = new TemplateImpl(engine, root.build(), templateId, variant); + Template template = new TemplateImpl(engine, root.build(), generatedId, variant); LOGGER.tracef("Parsing finished in %s ms", System.currentTimeMillis() - start); return template; } catch (IOException e) { - throw new IllegalStateException(e); + throw new TemplateException(e); } } @@ -137,8 +141,9 @@ private void processCharacter(char character) { tagCandidate(character); break; default: - throw new IllegalStateException("Unknown parsing state"); + parserError("unknown parsing state: " + state); } + lineCharacter++; } private void text(char character) { @@ -147,6 +152,7 @@ private void text(char character) { } else { if (isLineSeparator(character)) { line++; + lineCharacter = 1; } buffer.append(character); } @@ -175,6 +181,7 @@ private void tagCandidate(char character) { buffer.append(START_DELIMITER).append(character); if (isLineSeparator(character)) { line++; + lineCharacter = 1; } state = State.TEXT; } else if (character == START_DELIMITER) { @@ -197,7 +204,7 @@ private boolean isLineSeparator(char character) { private void flushText() { if (buffer.length() > 0 && !ignoreContent) { SectionBlock.Builder block = sectionBlockStack.peek(); - block.addNode(new TextNode(buffer.toString(), new OriginImpl(line, templateId, variant))); + block.addNode(new TextNode(buffer.toString(), origin())); } this.buffer = new StringBuilder(); } @@ -205,6 +212,7 @@ private void flushText() { private void flushTag() { state = State.TEXT; String content = buffer.toString(); + String tag = START_DELIMITER + content + END_DELIMITER; if (content.charAt(0) == Tag.SECTION.getCommand()) { @@ -216,7 +224,7 @@ private void flushTag() { Iterator iter = splitSectionParams(content); if (!iter.hasNext()) { - throw new IllegalStateException("No helper name"); + throw new TemplateException("No helper name"); } String sectionName = iter.next(); sectionName = sectionName.substring(1, sectionName.length()); @@ -236,7 +244,7 @@ private void flushTag() { // Add the new block SectionBlock.Builder block = SectionBlock.builder("" + sectionBlockIdx++, this); sectionBlockStack.addFirst(block.setLabel(sectionName)); - processParams(sectionName, iter); + processParams(tag, sectionName, iter); // Initialize the block Map typeInfos = typeInfoStack.peek(); @@ -256,17 +264,18 @@ private void flushTag() { // New section SectionHelperFactory factory = engine.getSectionHelperFactory(sectionName); if (factory == null) { - throw new IllegalStateException("No section helper for: " + sectionName); + parserError("no section helper found for " + tag); } paramsStack.addFirst(factory.getParameters()); SectionBlock.Builder mainBlock = SectionBlock.builder(SectionHelperFactory.MAIN_BLOCK_NAME, this); sectionBlockStack.addFirst(mainBlock); - processParams(SectionHelperFactory.MAIN_BLOCK_NAME, iter); + processParams(tag, SectionHelperFactory.MAIN_BLOCK_NAME, iter); // Init section block Map typeInfos = typeInfoStack.peek(); Map result = factory.initializeBlock(typeInfos, mainBlock); - SectionNode.Builder sectionNode = SectionNode.builder(sectionName, new OriginImpl(-1, templateId, variant)) + SectionNode.Builder sectionNode = SectionNode + .builder(sectionName, origin()) .setEngine(engine) .setHelperFactory(factory); @@ -299,17 +308,14 @@ private void flushTag() { && !section.helperName.equals(name)) { // Block end if (!name.isEmpty() && !block.getLabel().equals(name)) { - throw new IllegalStateException( - "Section block end tag does not match the start tag. Start: " + block.getLabel() + ", end: " - + name); + parserError("section block end tag [" + name + "] does not match the start tag [" + block.getLabel() + "]"); } section.addBlock(sectionBlockStack.pop().build()); ignoreContent = true; } else { // Section end if (!name.isEmpty() && !section.helperName.equals(name)) { - throw new IllegalStateException( - "Section end tag does not match the start tag. Start: " + section.helperName + ", end: " + name); + parserError("section end tag [" + name + "] does not match the start tag [" + section.helperName + "]"); } section = sectionStack.pop(); if (!ignoreContent) { @@ -331,13 +337,23 @@ private void flushTag() { typeInfos.put(key, "[" + value + "]"); } else { - sectionBlockStack.peek() - .addNode(new ExpressionNode(apply(content), engine, new OriginImpl(line, templateId, variant))); + sectionBlockStack.peek().addNode(new ExpressionNode(apply(content), engine, origin())); } this.buffer = new StringBuilder(); } - private void processParams(String label, Iterator iter) { + private void parserError(String message) { + StringBuilder builder = new StringBuilder("Parser error"); + if (!id.equals(generatedId)) { + builder.append(" in template [").append(id).append("]"); + } + builder.append(" on line ").append(line).append(": ") + .append(message); + throw new TemplateException(origin(), + builder.toString()); + } + + private void processParams(String tag, String label, Iterator iter) { Map params = new HashMap<>(); List factoryParams = paramsStack.peek().get(label); List paramValues = new ArrayList<>(); @@ -390,7 +406,7 @@ private void processParams(String label, Iterator iter) { List undeclaredParams = factoryParams.stream().filter(p -> !p.optional && !params.containsKey(p.name)) .collect(Collectors.toList()); if (!undeclaredParams.isEmpty()) { - throw new IllegalStateException("Undeclared section params: " + undeclaredParams); + parserError("mandatory section parameters not declared for " + tag + ": " + undeclaredParams); } params.forEach(sectionBlockStack.peek()::addParameter); @@ -420,7 +436,7 @@ static int getFirstDeterminingEqualsCharPosition(String part) { return -1; } - static Iterator splitSectionParams(String content) { + Iterator splitSectionParams(String content) { boolean stringLiteral = false; boolean listLiteral = false; @@ -459,8 +475,7 @@ && isListLiteralEnd(content.charAt(i))) { if (buffer.length() > 0) { if (stringLiteral || listLiteral) { - throw new IllegalStateException( - "Unterminated string or array literal detected"); + parserError("unterminated string or array literal detected"); } parts.add(buffer.toString()); } @@ -568,18 +583,26 @@ static boolean isBracket(char character) { @Override public Expression apply(String value) { - return parseExpression(value, typeInfoStack.peek(), new OriginImpl(line, templateId, variant)); + return parseExpression(value, typeInfoStack.peek(), origin()); + } + + Origin origin() { + return new OriginImpl(line, lineCharacter, id, generatedId, variant); } static class OriginImpl implements Origin { private final int line; + private final int lineCharacter; private final String templateId; + private final String templateGeneratedId; private final Optional variant; - OriginImpl(int line, String templateId, Optional variant) { + OriginImpl(int line, int lineCharacter, String templateId, String templateGeneratedId, Optional variant) { this.line = line; + this.lineCharacter = lineCharacter; this.templateId = templateId; + this.templateGeneratedId = templateGeneratedId; this.variant = variant; } @@ -588,18 +611,28 @@ public int getLine() { return line; } + @Override + public int getLineCharacter() { + return lineCharacter; + } + @Override public String getTemplateId() { return templateId; } + @Override + public String getTemplateGeneratedId() { + return templateGeneratedId; + } + public Optional getVariant() { return variant; } @Override public int hashCode() { - return Objects.hash(line, templateId); + return Objects.hash(line, templateGeneratedId, templateId, variant); } @Override @@ -614,13 +647,15 @@ public boolean equals(Object obj) { return false; } OriginImpl other = (OriginImpl) obj; - return line == other.line && Objects.equals(templateId, other.templateId); + return line == other.line + && Objects.equals(templateGeneratedId, other.templateGeneratedId) + && Objects.equals(templateId, other.templateId) && Objects.equals(variant, other.variant); } @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append("OriginImpl [line=").append(line).append(", templateId=").append(templateId).append("]"); + builder.append(templateId).append(" at line ").append(line); return builder.toString(); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java index 5200883d4642f..3dd319cf8ec50 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java @@ -20,6 +20,7 @@ static Builder builder(String helperName, Origin origin) { final List blocks; private final SectionHelper helper; + private final Origin origin; SectionNode(List blocks, SectionHelper helper, Origin origin) { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateException.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateException.java new file mode 100644 index 0000000000000..c93d5c4d97b48 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateException.java @@ -0,0 +1,32 @@ +package io.quarkus.qute; + +import io.quarkus.qute.TemplateNode.Origin; + +public class TemplateException extends RuntimeException { + + private static final long serialVersionUID = 1336799943548973690L; + + private final Origin origin; + + public TemplateException(Throwable cause) { + this(null, null, cause); + } + + public TemplateException(String message) { + this(null, message, null); + } + + public TemplateException(Origin origin, String message) { + this(origin, message, null); + } + + public TemplateException(Origin origin, String message, Throwable cause) { + super(message, cause); + this.origin = origin; + } + + public Origin getOrigin() { + return origin; + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateLocator.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateLocator.java index 2fd5ed4dfb78f..ae876f06a0032 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateLocator.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateLocator.java @@ -22,7 +22,7 @@ interface TemplateLocation { /** * A {@link Reader} instance produced by a locator is immediately closed right after the template content is parsed. * - * @return + * @return the reader */ Reader read(); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java index 07efdced7c616..c321f3893b9b1 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java @@ -38,8 +38,12 @@ public interface Origin { int getLine(); + int getLineCharacter(); + String getTemplateId(); + String getTemplateGeneratedId(); + Optional getVariant(); } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index b45685458a5ec..8c7fde42df145 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -1,9 +1,14 @@ package io.quarkus.qute; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; +import io.quarkus.qute.TemplateLocator.TemplateLocation; +import io.quarkus.qute.TemplateNode.Origin; +import java.io.Reader; +import java.io.StringReader; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; @@ -11,29 +16,20 @@ public class ParserTest { @Test public void testSectionEndValidation() { - Engine engine = Engine.builder().addDefaultSectionHelpers() - .build(); - try { - engine.parse("{#if test}Hello {name}!{/for}"); - fail(); - } catch (IllegalStateException expected) { - String message = expected.getMessage(); - assertTrue(message.contains("if")); - assertTrue(message.contains("for")); - } + assertParserError("{#if test}Hello {name}!{/for}", + "Parser error on line 1: section end tag [for] does not match the start tag [if]", 1); } @Test public void testUnterminatedTag() { - Engine engine = Engine.builder().addDefaultSectionHelpers() - .build(); - try { - engine.parse("{#if test}Hello {name}"); - fail(); - } catch (IllegalStateException expected) { - String message = expected.getMessage(); - assertTrue(message.contains("if")); - } + assertParserError("{#if test}Hello {name}", + "Parser error on line 1: unterminated section [if] detected", 1); + } + + @Test + public void testNonexistentHelper() { + assertParserError("Hello!\n {#foo test/}", + "Parser error on line 2: no section helper found for {#foo test/}", 2); } @Test @@ -97,6 +93,54 @@ public void testLines() { assertEquals(8, find(template.getExpressions(), "item.name").origin.getLine()); } + @Test + public void testNodeOrigin() { + Engine engine = Engine.builder().addDefaultSectionHelpers() + .build(); + Template template = engine.parse("12{foo}"); + Origin origin = find(template.getExpressions(), "foo").origin; + assertEquals(1, origin.getLine()); + } + + @Test + public void testWithTemplateLocator() { + Engine engine = Engine.builder().addDefaultSectionHelpers().addLocator(id -> Optional.of(new TemplateLocation() { + + @Override + public Reader read() { + return new StringReader("{#if}"); + } + + @Override + public Optional getVariant() { + return Optional.empty(); + } + + })).build(); + try { + engine.getTemplate("foo.html"); + fail("No parser error found"); + } catch (TemplateException expected) { + assertNotNull(expected.getOrigin()); + assertEquals( + "Parser error in template [foo.html] on line 1: mandatory section parameters not declared for {#if}: [Parameter [name=condition, defaultValue=null, optional=false]]", + expected.getMessage()); + } + } + + private void assertParserError(String template, String message, int line) { + Engine engine = Engine.builder().addDefaultSectionHelpers().build(); + try { + engine.parse(template); + fail("No parser error found"); + } catch (TemplateException expected) { + assertNotNull(expected.getOrigin()); + assertEquals(line, expected.getOrigin().getLine(), "Wrong line"); + assertEquals(message, + expected.getMessage()); + } + } + private void assertExpr(Set expressions, String value, int parts, String typeCheckInfo) { Expression expr = find(expressions, value); assertEquals(parts, expr.parts.size());