Repository: incubator-freemarker Updated Branches: refs/heads/2.3-gae f55f9d891 -> 01294537b
Cleaned up more lexer/parser logic related to the handling of tag-closer delimiters ('>' and ']'). This has yielded the following two change log entries (and some improvements in error message quality, but that's hardly noticeable): 1. When the incompatible_improvements setting is set to 2.3.28 (or greater), fixed legacy parser glitch where a tag can be closed with an illegal ] (when it's not part of an expression) despite that the tag syntax is set to angle brackets. For example <#if x] worked just like <#if x>. Note that it doesn't affect the legal usage of ], like <#if x[0]> works correctly without this fix as well. 2. Fixed parser bug that disallowed using > at the top-level inside an interpolation (${...}). It had the same reason why <#if x > y> doesn't work as naively expected, but there's no real ambiguity in ${x > y}, so now it's allowed. Note that ${(x > y)?c} and ${(x > y)?string('y', 'n')}, which are how booleans are commonly printed, have always worked, as the > operation is not on the top-level inside the interpolation. Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/01294537 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/01294537 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/01294537 Branch: refs/heads/2.3-gae Commit: 01294537b882d06e2ac7df5b5fff590bf45f5227 Parents: f55f9d8 Author: ddekany <ddek...@apache.org> Authored: Mon Mar 19 22:19:37 2018 +0100 Committer: ddekany <ddek...@apache.org> Committed: Mon Mar 19 22:19:37 2018 +0100 ---------------------------------------------------------------------- .../java/freemarker/template/Configuration.java | 4 + src/main/javacc/FTL.jj | 77 ++++++++++++++++---- src/manual/en_US/book.xml | 27 +++++++ .../core/InterpolationSyntaxTest.java | 33 +++++++-- .../core/ParsingErrorMessagesTest.java | 66 ++++++----------- .../templates/string-builtins3.ftl | 4 +- 6 files changed, 147 insertions(+), 64 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/01294537/src/main/java/freemarker/template/Configuration.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java index 3114747..5376dda 100644 --- a/src/main/java/freemarker/template/Configuration.java +++ b/src/main/java/freemarker/template/Configuration.java @@ -885,6 +885,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * (Of course, the parameter default value expression is still evaluated in the context of the called * macro or function.) Similarly, {@code .macro_caller_template_name} (which itself was added in 2.3.28), * when used in a macro call argument, won't be incorrectly evaluated in the context of the called macro. + * <li><p>Fixed legacy parser glitch where a tag can be closed with an illegal {@code ]} (when it's not part + * of an expression) despite that the tag syntax is set to angle brackets. For example {@code <#if x]} + * worked just like {@code <#if x>}. Note that it doesn't affect the legal usage of {@code ]}, like + * {@code <#if x[0]>} works correctly without this fix as well. * </ul> * </li> * </ul> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/01294537/src/main/javacc/FTL.jj ---------------------------------------------------------------------- diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj index 26f3368..630ef39 100644 --- a/src/main/javacc/FTL.jj +++ b/src/main/javacc/FTL.jj @@ -667,7 +667,7 @@ TOKEN_MGR_DECLS: if (!strictSyntaxMode) { // Legacy feature (or bug?): Tag syntax never gets estabilished in non-strict mode. - // We do establilish the naming convention though. + // We do establish the naming convention though. checkNamingConvention(tok, tokenNamingConvention); SwitchTo(newLexState); return; @@ -684,6 +684,24 @@ TOKEN_MGR_DECLS: // We only get here if this is a strict FTL tag. directiveSyntaxEstablished = true; + if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_28 + || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX) { + // For END_xxx tags, as they can't contain expressions, the whole tag is a single token. So this is the only + // chance to check if we got something inconsistent like `</#if]`. (We can't do this at the #CLOSE_TAG1 or + // such, as at that point it's possible that the tag syntax is not yet established.) + char lastChar = image.charAt(image.length() - 1); + // Is it an end tag? + if (lastChar == ']' || lastChar == '>') { + if (!squBracTagSyntax && lastChar != '>' || squBracTagSyntax && lastChar != ']') { + throw new TokenMgrError( + "The tag shouldn't end with \""+ lastChar + "\".", + TokenMgrError.LEXICAL_ERROR, + tok.beginLine, tok.beginColumn, + tok.endLine, tok.endColumn); + } + } // if end-tag + } + checkNamingConvention(tok, tokenNamingConvention); SwitchTo(newLexState); @@ -825,9 +843,6 @@ TOKEN_MGR_DECLS: } private void endInterpolation(Token closingTk) { - if (postInterpolationLexState == -1) { - throw newUnexpectedClosingTokenException(closingTk); - } SwitchTo(postInterpolationLexState); postInterpolationLexState = -1; } @@ -1298,11 +1313,19 @@ TOKEN: { if (bracketNesting > 0) { --bracketNesting; - } else if (interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX - && (postInterpolationLexState != -1 || !squBracTagSyntax)) { + } else if (interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX && postInterpolationLexState != -1) { endInterpolation(matchedToken); } else { - // Glitch where you can close any tag with `]`, like `<#if x]`. We keep it for backward compatibility. + // There's a legacy glitch where you can always close a tag with `]`, like `<#if x]`. We have to keep that + // working for backward compatibility, hence we don't always throw at !squBracTagSyntax: + if (!squBracTagSyntax + && (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_28 + || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX) + || postInterpolationLexState != -1 /* We're in an interpolation => We aren't in a tag */) { + throw newUnexpectedClosingTokenException(matchedToken); + } + + // Close tag, either legally or to emulate legacy glitch: matchedToken.kind = DIRECTIVE_END; if (inFTLHeader) { eatNewline(); @@ -1336,7 +1359,7 @@ TOKEN: { if (curlyBracketNesting > 0) { --curlyBracketNesting; - } else if (interpolationSyntax != SQUARE_BRACKET_INTERPOLATION_SYNTAX) { + } else if (interpolationSyntax != SQUARE_BRACKET_INTERPOLATION_SYNTAX && postInterpolationLexState != -1) { endInterpolation(matchedToken); } else { throw newUnexpectedClosingTokenException(matchedToken); @@ -1529,19 +1552,47 @@ TOKEN: { <DIRECTIVE_END : ">"> { - if (inFTLHeader) eatNewline(); - inFTLHeader = false; - if (squBracTagSyntax) { + if (inFTLHeader) { + eatNewline(); + inFTLHeader = false; + } + if (squBracTagSyntax || postInterpolationLexState != -1 /* We are in an interpolation */) { matchedToken.kind = NATURAL_GT; } else { + if (directiveSyntaxEstablished && ( incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_28 + || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX)) { + if (squBracTagSyntax) { + throw new TokenMgrError( + "The tag shouldn't end with \"" + image.charAt(image.length() - 1) + "\".", + TokenMgrError.LEXICAL_ERROR, + matchedToken.beginLine, matchedToken.beginColumn, + matchedToken.endLine, matchedToken.endColumn); + } + } + SwitchTo(DEFAULT); } } | <EMPTY_DIRECTIVE_END : "/>" | "/]"> { - if (inFTLHeader) eatNewline(); - inFTLHeader = false; + if (directiveSyntaxEstablished && (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_28 + || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX)) { + String image = matchedToken.image; + char lastChar = image.charAt(image.length() - 1); + if (!squBracTagSyntax && lastChar != '>' || squBracTagSyntax && lastChar != ']') { + throw new TokenMgrError( + "The tag shouldn't end with \""+ lastChar + "\".", + TokenMgrError.LEXICAL_ERROR, + matchedToken.beginLine, matchedToken.beginColumn, + matchedToken.endLine, matchedToken.endColumn); + } + } + + if (inFTLHeader) { + eatNewline(); + inFTLHeader = false; + } SwitchTo(DEFAULT); } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/01294537/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index ac58a2a..f06b759 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -27788,6 +27788,33 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </listitem> <listitem> + <para>When the <link + linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal> + setting</link> is set to 2.3.28 (or greater), fixed legacy + parser glitch where a tag can be closed with an illegal + <literal>]</literal> (when it's not part of an expression) + despite that the tag syntax is set to angle brackets. For + example <literal><#if x]</literal> worked just like + <literal><#if x></literal>. Note that it doesn't affect + the legal usage of <literal>]</literal>, like <literal><#if + x[0]></literal> works correctly without this fix as + well.</para> + </listitem> + + <listitem> + <para>Fixed parser bug that disallowed using + <literal>></literal> at the top-level inside an interpolation + (<literal>${...}</literal>). It had the same reason why + <literal><#if x > y></literal> doesn't work as naively + expected, but there's no real ambiguity in <literal>${x > + y}</literal>, so now it's allowed. Note that <literal>${(x > + y)?c}</literal> and <literal>${(x > y)?string('y', + 'n')}</literal>, which are how booleans are commonly printed, + have always worked, as the <literal>></literal> operation is + not on the top-level inside the interpolation.</para> + </listitem> + + <listitem> <para>Fixed incorrect listing of valid <literal>roundingMode</literal>-s in <link linkend="topic.extendedJavaDecimalFormat">extended Java decimal http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/01294537/src/test/java/freemarker/core/InterpolationSyntaxTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/InterpolationSyntaxTest.java b/src/test/java/freemarker/core/InterpolationSyntaxTest.java index 2322d99..9a56f13 100644 --- a/src/test/java/freemarker/core/InterpolationSyntaxTest.java +++ b/src/test/java/freemarker/core/InterpolationSyntaxTest.java @@ -7,6 +7,7 @@ import java.io.StringWriter; import org.junit.Test; +import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.test.TemplateTest; @@ -27,6 +28,9 @@ public class InterpolationSyntaxTest extends TemplateTest { assertOutput("<@r'${1} #{1} [=1]'?interpret />", "1 1 [=1]"); assertOutput("${'\"${1} #{1} [=1]\"'?eval}", "1 1 [=1]"); + + assertOutput("<#setting booleanFormat='y,n'>${2>1}", "y"); // Not an error since 2.3.28 + assertOutput("[#ftl][#setting booleanFormat='y,n']${2>1}", "y"); // Not an error since 2.3.28 } @Test @@ -78,7 +82,9 @@ public class InterpolationSyntaxTest extends TemplateTest { assertErrorContains("[=", "end of file"); assertErrorContains("[=1", "unclosed \"[\""); - assertErrorContains("[=1}", "\"}\"", "open"); + + assertOutput("<#setting booleanFormat='y,n'>[=2>1]", "y"); + assertOutput("[#ftl][#setting booleanFormat='y,n'][=2>1]", "y"); assertOutput("[='[\\=1]']", "[=1]"); assertOutput("[='[\\=1][=2]']", "12"); // Usual legacy interpolation escaping glitch... @@ -101,18 +107,35 @@ public class InterpolationSyntaxTest extends TemplateTest { @Test public void legacyTagSyntaxGlitchStillWorksTest() throws Exception { - String ftl = "<#if [true][0]]t<#else]f</#if]"; + String badFtl1 = "<#if [true][0]]OK</#if>"; + String badFtl2 = "<#if true>OK</#if]"; + String badFtl3 = "<#assign x = 'OK'/]${x}"; + String badFtl4 = " <#t/]OK\n"; + getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_27); for (int syntax : new int[] { LEGACY_INTERPOLATION_SYNTAX, DOLLAR_INTERPOLATION_SYNTAX }) { getConfiguration().setInterpolationSyntax(syntax); - assertOutput(ftl, "t"); + assertOutput(badFtl1, "OK"); + assertOutput(badFtl2, "OK"); + assertOutput(badFtl3, "OK"); + assertOutput(badFtl4, "OK"); } // Legacy tag closing glitch is not emulated with this: getConfiguration().setInterpolationSyntax(SQUARE_BRACKET_INTERPOLATION_SYNTAX); - assertErrorContains(ftl, "\"]\""); + assertErrorContains(badFtl1, "\"]\""); + assertErrorContains(badFtl2, "\"]\""); + assertErrorContains(badFtl3, "\"]\""); + assertErrorContains(badFtl4, "\"]\""); + + getConfiguration().setInterpolationSyntax(LEGACY_INTERPOLATION_SYNTAX); + getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_28); + assertErrorContains(badFtl1, "\"]\""); + assertErrorContains(badFtl2, "\"]\""); + assertErrorContains(badFtl3, "\"]\""); + assertErrorContains(badFtl4, "\"]\""); } - + @Test public void errorMessagesAreSquareBracketInterpolationSyntaxAwareTest() throws Exception { assertErrorContains("<#if ${x}></#if>", "${...}", "${myExpression}"); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/01294537/src/test/java/freemarker/core/ParsingErrorMessagesTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/ParsingErrorMessagesTest.java b/src/test/java/freemarker/core/ParsingErrorMessagesTest.java index e471a31..0081662 100644 --- a/src/test/java/freemarker/core/ParsingErrorMessagesTest.java +++ b/src/test/java/freemarker/core/ParsingErrorMessagesTest.java @@ -19,23 +19,23 @@ package freemarker.core; -import static org.junit.Assert.*; - -import java.io.IOException; +import static freemarker.template.Configuration.*; import org.junit.Test; import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.utility.StringUtil; +import freemarker.test.TemplateTest; -public class ParsingErrorMessagesTest { +public class ParsingErrorMessagesTest extends TemplateTest { - private Configuration cfg = new Configuration(Configuration.VERSION_2_3_21); - { + @Override + protected Configuration createConfiguration() throws Exception { + Configuration cfg = super.createConfiguration(); + cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_21); cfg.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX); + return cfg; } - + @Test public void testNeedlessInterpolation() { assertErrorContains("<#if ${x} == 3></#if>", "instead of ${"); @@ -77,45 +77,23 @@ public class ParsingErrorMessagesTest { } @Test - public void testInterpolatingClosingsErrors() { - assertErrorContains("${x", "unclosed"); + public void testInterpolatingClosingsErrors() throws Exception { + assertErrorContains("<#ftl>${x", "unclosed"); assertErrorContains("<#assign x = x}>", "\"}\"", "open"); - // TODO assertErrorContains("<#assign x = '${x'>", "unclosed"); - } - - private void assertErrorContains(String ftl, String... expectedSubstrings) { - assertErrorContains(false, ftl, expectedSubstrings); - assertErrorContains(true, ftl, expectedSubstrings); - } - - private void assertErrorContains(boolean squareTags, String ftl, String... expectedSubstrings) { - try { - if (squareTags) { - ftl = ftl.replace('<', '[').replace('>', ']'); - } - new Template("adhoc", ftl, cfg); - fail("The tempalte had to fail"); - } catch (ParseException e) { - String msg = e.getMessage(); - for (String needle: expectedSubstrings) { - if (needle.startsWith("\\!")) { - String netNeedle = needle.substring(2); - if (msg.contains(netNeedle)) { - fail("The message shouldn't contain substring " + StringUtil.jQuote(netNeedle) + ":\n" + msg); - } - } else if (!msg.contains(needle)) { - fail("The message didn't contain substring " + StringUtil.jQuote(needle) + ":\n" + msg); - } - } - showError(e); - } catch (IOException e) { - // Won't happen - throw new RuntimeException(e); + assertOutput("<#assign x = '${x'>", ""); // Legacy glitch... should fail in theory. + + for (int syntax : new int[] { LEGACY_INTERPOLATION_SYNTAX, DOLLAR_INTERPOLATION_SYNTAX }) { + getConfiguration().setInterpolationSyntax(syntax); + assertErrorContains("<#ftl>${'x']", "\"]\"", "open"); + super.assertErrorContains("<#ftl>${'x'>", "end of file"); + super.assertErrorContains("[#ftl]${'x'>", "end of file"); } } - private void showError(Throwable e) { - //System.out.println(e); + protected Throwable assertErrorContains(String ftl, String... expectedSubstrings) { + super.assertErrorContains(ftl, expectedSubstrings); + ftl = ftl.replace('<', '[').replace('>', ']'); + return super.assertErrorContains(ftl, expectedSubstrings); } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/01294537/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl ---------------------------------------------------------------------- diff --git a/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl b/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl index 77389fa..06e7dd7 100644 --- a/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl +++ b/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl @@ -198,7 +198,7 @@ <@assertEquals expected='aa' actual=27?lower_abc /> <@assertEquals expected='ab' actual=28?lower_abc /> <@assertEquals expected='cv' actual=100?lower_abc /> -<@assertFails messageRegexp='0|at least 1']> +<@assertFails messageRegexp='0|at least 1'> ${0?lower_abc} </@assertFails> <@assertFails messageRegexp='0|at least 1'> @@ -214,7 +214,7 @@ <@assertEquals expected='AA' actual=27?upper_abc /> <@assertEquals expected='AB' actual=28?upper_abc /> <@assertEquals expected='CV' actual=100?upper_abc /> -<@assertFails messageRegexp='0|at least 1']> +<@assertFails messageRegexp='0|at least 1'> ${0?upper_abc} </@assertFails> <@assertFails messageRegexp='0|at least 1'>