More helpful error messages if someone uses non-camel case names.

Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/51e4bfe3
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/51e4bfe3
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/51e4bfe3

Branch: refs/heads/3
Commit: 51e4bfe3c9cd2a2063860978c7f7e9b48c64871c
Parents: a04ab89
Author: ddekany <ddek...@apache.org>
Authored: Sun Jul 16 17:00:52 2017 +0200
Committer: ddekany <ddek...@apache.org>
Committed: Sun Jul 16 17:00:52 2017 +0200

----------------------------------------------------------------------
 .../core/FM2ASTToFM3SourceConverter.java        | 10 +--
 .../freemarker/converter/ConverterUtils.java    | 28 -------
 .../freemarker/converter/ConverterUtilTest.java | 18 -----
 .../core/ParsingErrorMessagesTest.java          | 41 +++++++++-
 .../freemarker/core/SpecialVariableTest.java    |  2 +-
 .../freemarker/core/util/StringUtilTest.java    | 15 +++-
 .../apache/freemarker/core/ASTDirSetting.java   | 43 +++++++---
 .../apache/freemarker/core/ASTExpBuiltIn.java   | 67 +++++++++++-----
 .../freemarker/core/ASTExpBuiltInVariable.java  | 55 +++++++------
 .../org/apache/freemarker/core/MessageUtil.java |  3 +
 .../freemarker/core/util/_StringUtil.java       | 48 ++++++-----
 freemarker-core/src/main/javacc/FTL.jj          | 84 ++++++++++++++------
 12 files changed, 249 insertions(+), 165 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
----------------------------------------------------------------------
diff --git 
a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
 
b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
index 03eb9c5..c60c31f 100644
--- 
a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
+++ 
b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
@@ -231,7 +231,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private String convertFtlHeaderParamName(String name) throws 
ConverterException {
-        String converted = name.indexOf('_') == -1 ? name : 
ConverterUtils.snakeCaseToCamelCase(name);
+        String converted = name.indexOf('_') == -1 ? name : 
_StringUtil.snakeCaseToCamelCase(name);
         if (converted.equals("attributes")) {
             converted = "customSettings";
         }
@@ -605,7 +605,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private String convertSettingName(String name, TemplateObject node) throws 
ConverterException {
-        String converted = name.indexOf('_') == -1 ? name : 
ConverterUtils.snakeCaseToCamelCase(name);
+        String converted = name.indexOf('_') == -1 ? name : 
_StringUtil.snakeCaseToCamelCase(name);
 
         if (converted.equals("classicCompatible")) {
             throw new UnconvertableLegacyFeatureException("There 
\"classicCompatible\" setting doesn't exist in "
@@ -1513,7 +1513,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private String convertBuiltInVariableName(String name) throws 
ConverterException {
-        String converted = name.indexOf('_') == -1 ? name : 
ConverterUtils.snakeCaseToCamelCase(name);
+        String converted = name.indexOf('_') == -1 ? name : 
_StringUtil.snakeCaseToCamelCase(name);
 
         // Will replace removed names here
 
@@ -1546,7 +1546,7 @@ public class FM2ASTToFM3SourceConverter {
     static {
         Map<String, String> domKeyMapping = new HashMap<>();
         for (String atAtKey : AtAtKeyAccessor.getAtAtKeys()) {
-            String atAtKeyCC = ConverterUtils.snakeCaseToCamelCase(atAtKey);
+            String atAtKeyCC = _StringUtil.snakeCaseToCamelCase(atAtKey);
             if (!atAtKeyCC.equals(atAtKey)) {
                 domKeyMapping.put(atAtKey, atAtKeyCC);
             }
@@ -1858,7 +1858,7 @@ public class FM2ASTToFM3SourceConverter {
     private String convertBuiltInName(String name) throws ConverterException {
         String converted = IRREGULAR_BUILT_IN_NAME_CONVERSIONS.get(name);
         if (converted == null) {
-            converted = name.indexOf('_') == -1 ? name : 
ConverterUtils.snakeCaseToCamelCase(name);
+            converted = name.indexOf('_') == -1 ? name : 
_StringUtil.snakeCaseToCamelCase(name);
         }
 
         if (!fm3BuiltInNames.contains(converted)) {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterUtils.java
----------------------------------------------------------------------
diff --git 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterUtils.java
 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterUtils.java
index aa837b6..cbe3866 100644
--- 
a/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterUtils.java
+++ 
b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterUtils.java
@@ -25,34 +25,6 @@ public final class ConverterUtils {
         //
     }
 
-    public static String snakeCaseToCamelCase(String s) {
-        if (s == null) {
-            return null;
-        }
-
-        int wordEndIdx = s.indexOf('_');
-        if (wordEndIdx == -1) {
-            return s.toLowerCase();
-        }
-
-        StringBuilder sb = new StringBuilder(s.length());
-        int wordStartIdx = 0;
-        do {
-            if (wordStartIdx < wordEndIdx) {
-                char wordStartC = s.charAt(wordStartIdx);
-                sb.append(sb.length() != 0 ? Character.toUpperCase(wordStartC) 
: Character.toLowerCase(wordStartC));
-                sb.append(s.substring(wordStartIdx + 1, 
wordEndIdx).toLowerCase());
-            }
-
-            wordStartIdx = wordEndIdx + 1;
-            wordEndIdx = s.indexOf('_', wordStartIdx);
-            if (wordEndIdx == -1) {
-                wordEndIdx = s.length();
-            }
-        } while (wordStartIdx < s.length());
-        return sb.toString();
-    }
-
     public static boolean isUpperCaseLetter(char c) {
         return Character.isUpperCase(c) && Character.isLetter(c);
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-converter/src/test/java/org/freemarker/converter/ConverterUtilTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-converter/src/test/java/org/freemarker/converter/ConverterUtilTest.java
 
b/freemarker-converter/src/test/java/org/freemarker/converter/ConverterUtilTest.java
index 811ea18..3646834 100644
--- 
a/freemarker-converter/src/test/java/org/freemarker/converter/ConverterUtilTest.java
+++ 
b/freemarker-converter/src/test/java/org/freemarker/converter/ConverterUtilTest.java
@@ -19,24 +19,6 @@
 
 package org.freemarker.converter;
 
-import static junit.framework.TestCase.assertNull;
-import static org.junit.Assert.assertEquals;
-
-import org.apache.freemarker.converter.ConverterUtils;
-import org.junit.Test;
-
 public class ConverterUtilTest {
 
-    @Test
-    public void snakeCaseToCamelCase() {
-        assertNull(ConverterUtils.snakeCaseToCamelCase(null));
-        assertEquals("", ConverterUtils.snakeCaseToCamelCase(""));
-        assertEquals("x", ConverterUtils.snakeCaseToCamelCase("x"));
-        assertEquals("xxx", ConverterUtils.snakeCaseToCamelCase("xXx"));
-        assertEquals("fooBar", ConverterUtils.snakeCaseToCamelCase("foo_bar"));
-        assertEquals("fooBar", ConverterUtils.snakeCaseToCamelCase("FOO_BAR"));
-        assertEquals("fooBar", 
ConverterUtils.snakeCaseToCamelCase("_foo__bar_"));
-        assertEquals("aBC", ConverterUtils.snakeCaseToCamelCase("a_b_c"));
-    }
-
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
index de746d5..3e77769 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
@@ -45,6 +45,10 @@ public class ParsingErrorMessagesTest {
         assertErrorContains("<#foo />", "nknown directive", "#foo");
         assertErrorContains("<#set x = 1 />", "nknown directive", "#set", 
"#assign");
         assertErrorContains("<#iterator></#iterator>", "nknown directive", 
"#iterator", "#list");
+        assertErrorContains("<#if x><#elseif y></#if>", "nknown directive", 
"#elseIf");
+        assertErrorContains("<#outputformat 'HTML'></#outputformat>", "nknown 
directive", "#outputFormat");
+        assertErrorContains("<#autoesc></#autoesc>", "nknown directive", 
"#autoEsc");
+        assertErrorContains("<#noautoesc></#noautoesc>", "nknown directive", 
"#noAutoEsc");
     }
 
     @Test
@@ -70,7 +74,42 @@ public class ParsingErrorMessagesTest {
         assertErrorContains("${(blah", "\"(\"", "unclosed");
         assertErrorContains("${blah", "\"{\"", "unclosed");
     }
-    
+
+    @Test
+    public void testBuiltInWrongNames() {
+        assertErrorContains("${x?lower_case}", "camel case", "The correct name 
is: lowerCase");
+        assertErrorContains("${x?iso_utc_nz}", "camel case", "The correct name 
is: isoUtcNZ");
+        assertErrorContains("${x?no_such_name}", "camel case", "\\!The correct 
name is:", "alphabetical list");
+        assertErrorContains("${x?nosuchname}", "\\!camel case", "\\!The 
correct name is:", "alphabetical list");
+    }
+
+    @Test
+    public void testSettingWrongNames() {
+        assertErrorContains("<#setting time_format='HHmm'>", "camel case", 
"The correct name is: timeFormat",
+                "\\!setting names are:");
+        assertErrorContains("<#setting no_such_name=1>", "camel case", "\\!The 
correct name is:",
+                "setting names are:");
+        assertErrorContains("<#setting nosuchname=1>", "\\!The correct name 
is:", "\\!camel case",
+                "setting names are:");
+    }
+
+    @Test
+    public void testSpecialVariableWrongNames() {
+        assertErrorContains("${.data_model}", "camel case", "The correct name 
is: dataModel",
+                "\\!variable names are:");
+        assertErrorContains("${.no_such_name}", "camel case", "\\!The correct 
name is:",
+                "variable names are:");
+        assertErrorContains("${.nosuchname}", "\\!camel case", "\\!The correct 
name is:",
+                "variable names are:");
+    }
+
+    @Test
+    public void testFtlParameterWrongNames() {
+        assertErrorContains("<#ftl strip_whitespace=false>", "camel case", 
"The correct name is: stripWhitespace");
+        assertErrorContains("<#ftl no_such_name=1>", "camel case", "\\!The 
correct name is:");
+        assertErrorContains("<#ftl nosuchname=1>", "\\!camel case", "\\!The 
correct name is:");
+    }
+
     @Test
     public void testInterpolatingClosingsErrors() {
         assertErrorContains("${x", "unclosed");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core-test/src/test/java/org/apache/freemarker/core/SpecialVariableTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/SpecialVariableTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/SpecialVariableTest.java
index 664de58..270701a 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/SpecialVariableTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/SpecialVariableTest.java
@@ -118,7 +118,7 @@ public class SpecialVariableTest extends TemplateTest {
                 "false true false "
                 + "true false true false true");
         
-        assertErrorContains("${.autoEscaping}", "You may meant: \"autoEsc\"");
+        assertErrorContains("${.autoEscaping}", "The correct name is: 
autoEsc");
     }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
index 2a0ae9d..1d7043c 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringUtilTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.freemarker.core.util;
 
+import static junit.framework.TestCase.assertNull;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
@@ -399,5 +400,17 @@ public class StringUtilTest {
         assertEquals("x\ny", _StringUtil.normalizeEOLs("x\ry"));
         assertEquals("\n\n\n\n\n\n", 
_StringUtil.normalizeEOLs("\n\r\r\r\n\r\n\r"));
     }
-    
+
+    @Test
+    public void snakeCaseToCamelCase() {
+        assertNull(_StringUtil.snakeCaseToCamelCase(null));
+        assertEquals("", _StringUtil.snakeCaseToCamelCase(""));
+        assertEquals("x", _StringUtil.snakeCaseToCamelCase("x"));
+        assertEquals("xxx", _StringUtil.snakeCaseToCamelCase("xXx"));
+        assertEquals("fooBar", _StringUtil.snakeCaseToCamelCase("foo_bar"));
+        assertEquals("fooBar", _StringUtil.snakeCaseToCamelCase("FOO_BAR"));
+        assertEquals("fooBar", _StringUtil.snakeCaseToCamelCase("_foo__bar_"));
+        assertEquals("aBC", _StringUtil.snakeCaseToCamelCase("a_b_c"));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
index 0a8a160..134f6fe 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSetting.java
@@ -19,12 +19,13 @@
 
 package org.apache.freemarker.core;
 
-import java.util.Arrays;
+import java.util.Set;
 
 import org.apache.freemarker.core.model.TemplateBooleanModel;
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateNumberModel;
 import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util._SortedArraySet;
 import org.apache.freemarker.core.util._StringUtil;
 
 /**
@@ -35,7 +36,7 @@ final class ASTDirSetting extends ASTDirective {
     private final String key;
     private final ASTExpression value;
     
-    static final String[] SETTING_NAMES = new String[] {
+    static final Set<String> SETTING_NAMES = new _SortedArraySet<String>(
             // Must be sorted alphabetically!
             MutableProcessingConfiguration.BOOLEAN_FORMAT_KEY,
             MutableProcessingConfiguration.DATE_FORMAT_KEY,
@@ -46,13 +47,13 @@ final class ASTDirSetting extends ASTDirective {
             MutableProcessingConfiguration.SQL_DATE_AND_TIME_TIME_ZONE_KEY,
             MutableProcessingConfiguration.TIME_FORMAT_KEY,
             MutableProcessingConfiguration.TIME_ZONE_KEY,
-            MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY,
-    };
+            MutableProcessingConfiguration.URL_ESCAPING_CHARSET_KEY
+    );
 
     ASTDirSetting(Token keyTk, FMParserTokenManager tokenManager, 
ASTExpression value, Configuration cfg)
             throws ParseException {
         String key = keyTk.image;
-        if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
+        if (!SETTING_NAMES.contains(key)) {
             StringBuilder sb = new StringBuilder();
             if 
(Configuration.ExtendableBuilder.getSettingNames().contains(key)) {
                 sb.append("The setting name is recognized, but changing this 
setting from inside a template isn't "
@@ -60,17 +61,33 @@ final class ASTDirSetting extends ASTDirective {
             } else {
                 sb.append("Unknown setting name: ");
                 sb.append(_StringUtil.jQuote(key)).append(".");
-                sb.append(" The allowed setting names are: ");
 
-                boolean first = true;
-                for (String correctName : SETTING_NAMES) {
-                    if (first) {
-                        first = false;
-                    } else {
-                        sb.append(", ");
+                String correctedKey;
+                if (key.indexOf('_') != -1) {
+                    sb.append(MessageUtil.FM3_SNAKE_CASE);
+                    correctedKey = _StringUtil.snakeCaseToCamelCase(key);
+                    if (!SETTING_NAMES.contains(correctedKey)) {
+                        correctedKey = null;
                     }
+                } else {
+                    correctedKey = null;
+                }
+
+                if (correctedKey != null) {
+                    sb.append("\nThe correct name is: ").append(correctedKey);
+                } else {
+                    sb.append("\nThe allowed setting names are: ");
 
-                    sb.append(correctName);
+                    boolean first = true;
+                    for (String correctName : SETTING_NAMES) {
+                        if (first) {
+                            first = false;
+                        } else {
+                            sb.append(", ");
+                        }
+
+                        sb.append(correctName);
+                    }
                 }
             }
             throw new ParseException(sb.toString(), null, keyTk);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
index 3f0454f..a6b2b77 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
@@ -304,34 +304,57 @@ abstract class ASTExpBuiltIn extends ASTExpression 
implements Cloneable {
         String key = keyTk.image;
         ASTExpBuiltIn bi = BUILT_INS_BY_NAME.get(key);
         if (bi == null) {
-            StringBuilder buf = new StringBuilder("Unknown built-in: 
").append(_StringUtil.jQuote(key)).append(". ");
-            
-            buf.append(
-                    "Help (latest version): 
http://freemarker.org/docs/ref_builtins.html; "
-                    + "you're using FreeMarker 
").append(Configuration.getVersion()).append(".\n" 
-                    + "The alphabetical list of built-ins:");
-            List<String> names = new 
ArrayList<>(BUILT_INS_BY_NAME.keySet().size());
-            names.addAll(BUILT_INS_BY_NAME.keySet());
-            Collections.sort(names);
-            char lastLetter = 0;
+            StringBuilder sb = new StringBuilder("Unknown built-in: 
").append(_StringUtil.jQuote(key)).append(".");
 
-            boolean first = true;
-            for (String correctName : names) {
-                if (first) {
-                    first = false;
-                } else {
-                    buf.append(", ");
+            String correctedKey;
+            if (key.indexOf("_") != -1) {
+                sb.append(MessageUtil.FM3_SNAKE_CASE);
+                correctedKey = _StringUtil.snakeCaseToCamelCase(key);
+                if (!BUILT_INS_BY_NAME.containsKey(correctedKey)) {
+                    if (correctedKey.length() > 1) {
+                        correctedKey = correctedKey.substring(0, 
correctedKey.length() - 2)
+                                + correctedKey.substring(correctedKey.length() 
- 2).toUpperCase();
+                        if (!BUILT_INS_BY_NAME.containsKey(correctedKey)) {
+                            correctedKey = null;
+                        }
+                    } else {
+                        correctedKey = null;
+                    }
                 }
+            } else {
+                correctedKey = null;
+            }
+
+            if (correctedKey != null) {
+                sb.append("\nThe correct name is: ").append(correctedKey);
+            } else {
+                sb.append(
+                        "\nHelp (latest version): 
http://freemarker.org/docs/ref_builtins.html; "
+                                + "you're using FreeMarker 
").append(Configuration.getVersion()).append(".\n"
+                        + "The alphabetical list of built-ins:");
+                List<String> names = new 
ArrayList<>(BUILT_INS_BY_NAME.keySet().size());
+                names.addAll(BUILT_INS_BY_NAME.keySet());
+                Collections.sort(names);
+                char lastLetter = 0;
+
+                boolean first = true;
+                for (String correctName : names) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(", ");
+                    }
 
-                char firstChar = correctName.charAt(0);
-                if (firstChar != lastLetter) {
-                    lastLetter = firstChar;
-                    buf.append('\n');
+                    char firstChar = correctName.charAt(0);
+                    if (firstChar != lastLetter) {
+                        lastLetter = firstChar;
+                        sb.append('\n');
+                    }
+                    sb.append(correctName);
                 }
-                buf.append(correctName);
             }
                 
-            throw new ParseException(buf.toString(), null, keyTk);
+            throw new ParseException(sb.toString(), null, keyTk);
         }
         
         try {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
index c7afcda..f2b2537 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
@@ -20,8 +20,8 @@
 package org.apache.freemarker.core;
 
 import java.nio.charset.Charset;
-import java.util.Arrays;
 import java.util.Date;
+import java.util.Set;
 
 import org.apache.freemarker.core.model.TemplateDateModel;
 import org.apache.freemarker.core.model.TemplateHashModel;
@@ -29,6 +29,7 @@ import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateModelException;
 import org.apache.freemarker.core.model.impl.SimpleDate;
 import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._SortedArraySet;
 import org.apache.freemarker.core.util._StringUtil;
 
 /**
@@ -60,7 +61,8 @@ final class ASTExpBuiltInVariable extends ASTExpression {
     static final String URL_ESCAPING_CHARSET = "urlEscapingCharset";
     static final String NOW = "now";
     
-    static final String[] BUILT_IN_VARIABLE_NAMES = new String[] {
+    static final Set<String> BUILT_IN_VARIABLE_NAMES = new _SortedArraySet<>(
+        // Must be sorted alphabetically!
         AUTO_ESC,
         CURRENT_NODE,
         CURRENT_TEMPLATE_NAME,
@@ -84,7 +86,7 @@ final class ASTExpBuiltInVariable extends ASTExpression {
         URL_ESCAPING_CHARSET,
         VARS,
         VERSION
-    };
+    );
 
     private final String name;
     private final TemplateModel parseTimeValue;
@@ -93,35 +95,38 @@ final class ASTExpBuiltInVariable extends ASTExpression {
             throws ParseException {
         String name = nameTk.image;
         this.parseTimeValue = parseTimeValue;
-        if (Arrays.binarySearch(BUILT_IN_VARIABLE_NAMES, name) < 0) {
+        if (!BUILT_IN_VARIABLE_NAMES.contains(name)) {
             StringBuilder sb = new StringBuilder();
             sb.append("Unknown special variable name: ");
             sb.append(_StringUtil.jQuote(name)).append(".");
 
-            {
-                String correctName;
-                if (
-                        name.equals("auto_escape") || 
name.equals("auto_escaping") || name.equals("autoEsc") ||
-                        name.equals("autoEscape") || 
name.equals("autoEscaping")) {
-                    correctName = "autoEsc";
-                } else {
-                    correctName = null;
-                }
-                if (correctName != null) {
-                    sb.append(" You may meant: ");
-                    sb.append(_StringUtil.jQuote(correctName)).append(".");
+            String correctedName;
+            if (name.indexOf('_') != -1) {
+                sb.append(MessageUtil.FM3_SNAKE_CASE);
+                correctedName = _StringUtil.snakeCaseToCamelCase(name);
+                if (!BUILT_IN_VARIABLE_NAMES.contains(correctedName)) {
+                    correctedName = null;
                 }
+            } else if (name.equals("auto_escape") || 
name.equals("auto_escaping") || name.equals("autoEsc")
+                    || name.equals("autoEscape") || 
name.equals("autoEscaping")) {
+                correctedName = "autoEsc";
+            } else {
+                correctedName = null;
             }
-            
-            sb.append("\nThe allowed special variable names are: ");
-            boolean first = true;
-            for (final String correctName : BUILT_IN_VARIABLE_NAMES) {
-                if (first) {
-                    first = false;
-                } else {
-                    sb.append(", ");
+
+            if (correctedName != null) {
+                sb.append("\nThe correct name is: ").append(correctedName);
+            } else {
+                sb.append("\nThe supported special variable names are: ");
+                boolean first = true;
+                for (final String supportedName : BUILT_IN_VARIABLE_NAMES) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(", ");
+                    }
+                    sb.append(supportedName);
                 }
-                sb.append(correctName);
             }
             throw new ParseException(sb.toString(), null, nameTk);
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
index 6a2bc2f..8a90159 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java
@@ -47,6 +47,9 @@ class MessageUtil {
             + "to specify which fields to display. "
     };
 
+    static final String FM3_SNAKE_CASE = "\nThe name contains '_' character, 
but since FreeMarker 3 names defined "
+            + "by the template language use camel case (e.g. 
someExampleName).";
+
     static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n";
 
     static final String EMBEDDED_MESSAGE_END = "\n---end-message---";

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
index 08d7870..57ba42c 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_StringUtil.java
@@ -1573,33 +1573,31 @@ public class _StringUtil {
         }
     }
 
-    // [2.4] Won't be needed anymore
-    /**
-     * A deliberately very inflexible camel case to underscored converter; it 
must not convert improper camel case
-     * names to a proper underscored name.
-     */
-    public static String camelCaseToUnderscored(String camelCaseName) {
-        int i = 0;
-        while (i < camelCaseName.length() && 
Character.isLowerCase(camelCaseName.charAt(i))) {
-            i++;
+    public static String snakeCaseToCamelCase(String s) {
+        if (s == null) {
+            return null;
         }
-        if (i == camelCaseName.length()) {
-            // No conversion needed
-            return camelCaseName;
+
+        int wordEndIdx = s.indexOf('_');
+        if (wordEndIdx == -1) {
+            return s.toLowerCase();
         }
-        
-        StringBuilder sb = new StringBuilder();
-        sb.append(camelCaseName.substring(0, i));
-        while (i < camelCaseName.length()) {
-            final char c = camelCaseName.charAt(i);
-            if (_StringUtil.isUpperUSASCII(c)) {
-                sb.append('_');
-                sb.append(Character.toLowerCase(c));
-            } else {
-                sb.append(c);
+
+        StringBuilder sb = new StringBuilder(s.length());
+        int wordStartIdx = 0;
+        do {
+            if (wordStartIdx < wordEndIdx) {
+                char wordStartC = s.charAt(wordStartIdx);
+                sb.append(sb.length() != 0 ? Character.toUpperCase(wordStartC) 
: Character.toLowerCase(wordStartC));
+                sb.append(s.substring(wordStartIdx + 1, 
wordEndIdx).toLowerCase());
             }
-            i++;
-        }
+
+            wordStartIdx = wordEndIdx + 1;
+            wordEndIdx = s.indexOf('_', wordStartIdx);
+            if (wordEndIdx == -1) {
+                wordEndIdx = s.length();
+            }
+        } while (wordStartIdx < s.length());
         return sb.toString();
     }
 
@@ -1623,5 +1621,5 @@ public class _StringUtil {
         }
         return NORMALIZE_EOLS_REGEXP.matcher(s).replaceAll("\n");
     }
-    
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51e4bfe3/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj 
b/freemarker-core/src/main/javacc/FTL.jj
index c289b74..37a66ae 100644
--- a/freemarker-core/src/main/javacc/FTL.jj
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -841,8 +841,8 @@ TOKEN:
             if (dn.equals("set") || dn.equals("var")) {
                 tip = "Use #assign or #local or #global, depending on the 
intented scope "
                       + "(#assign is template-scope). " + 
PLANNED_DIRECTIVE_HINT;
-            } else if (dn.equals("else_if") || dn.equals("elif")) {
-               tip = "Use #elseIf.";
+            } else if (dn.equals("else_if") || dn.equals("elif") || 
dn.equals("elseif")) {
+               tip = "Use #elseIf instead.";
             } else if (dn.equals("no_escape")) {
                tip = "Use #noEscape instead.";
             } else if (dn.equals("method")) {
@@ -859,6 +859,14 @@ TOKEN:
                 tip = "You may meant #items.";
             } else if (dn.equals("separator") || dn.equals("separate") || 
dn.equals("separ")) {
                 tip = "You may meant #sep.";
+            } else if (dn.equals("outputformat")) {
+               tip = "Use #outputFormat instead.";
+            } else if (dn.equals("noautoesc")) {
+               tip = "Use #noAutoEsc instead.";
+            } else if (dn.equals("autoesc")) {
+               tip = "Use #autoEsc instead.";
+            } else if (dn.equals("noparse")) {
+               tip = "Use #noParse instead.";
             } else {
                 tip = "Help (latest version): 
http://freemarker.org/docs/ref_directive_alphaidx.html; "
                         + "you're using FreeMarker " + 
Configuration.getVersion() + ".";
@@ -3857,40 +3865,64 @@ void HeaderElement() :
                             } catch (TemplateModelException tme) {
                             }
                         } else {
+                            StringBuilder sb = new StringBuilder();
+                            sb.append("Unknown header parameter: ").append(ks);
+
+                            if (ks.indexOf('_') != -1) {
+                                sb.append(MessageUtil.FM3_SNAKE_CASE);
+                            }
+
                             String correctName;
-                            String ksLC = ks.toLowerCase();
-                               if (ksLC.equals("charset") || 
ksLC.equals("source_encoding")
-                                       || ksLC.equals("sourcerncoding")) {
+                            switch (ks.toLowerCase()) {
+                               case "charset":
+                               case "source_encoding":
+                               case "sourcerncoding":
                                    correctName = "encoding";
-                               } else if (ksLC.equals("attributes") || 
ksLC.equals("customsettings")
-                                       || ksLC.equals("custom_settings") || 
ksLC.equals("settings")) {
+                                   break;
+                               case "attributes":
+                               case "customsettings":
+                            case "custom_settings":
+                            case "settings":
                                    correctName = "customSettings";
-                               } else if 
(ksLC.equalsIgnoreCase("strip_whitespace")
-                                       || 
ksLC.equalsIgnoreCase("stripwhitespace")
-                                       || 
ksLC.equalsIgnoreCase("remove_whitespace")
-                                       || 
ksLC.equalsIgnoreCase("removewhitespace")) {
+                                   break;
+                               case "strip_whitespace":
+                            case "stripwhitespace":
+                            case "remove_whitespace":
+                            case "removewhitespace":
                                    correctName = "stripWhitespace";
-                               } else if (ksLC.equalsIgnoreCase("strip_text") 
|| ksLC.equalsIgnoreCase("striptext")
-                                       || ksLC.equalsIgnoreCase("remove_text") 
|| ksLC.equalsIgnoreCase("removetext")) {
+                                   break;
+                               case "strip_text":
+                               case "striptext":
+                            case "remove_text":
+                            case "removetext":
                                    correctName = "stripText";
-                               } else if (ksLC.equals("xmlns") || 
ksLC.equalsIgnoreCase("ns_prefixes")
-                                       || ksLC.equalsIgnoreCase("nsprefixes")) 
{
+                                   break;
+                               case "xmlns":
+                               case "ns_prefixes":
+                               case "nsprefixes":
                                 correctName = "nsPrefixes";
-                               } else if 
(ksLC.equalsIgnoreCase("output_format")
-                                       || 
ksLC.equalsIgnoreCase("outputformat")) {
+                                break;
+                               case "output_format":
+                            case "outputformat":
                                 correctName = "outputFormat";
-                            } else if (ksLC.equals("autoEscape") || 
ksLC.equals("autoEscaping")
-                                    || ksLC.equals("autoesc")
-                                    || ksLC.equals("auto_escape") || 
ksLC.equals("auto_escaping")
-                                    || ksLC.equalsIgnoreCase("auto_esc")) {
+                                break;
+                            case "autoescape":
+                            case "autoescaping":
+                            case "autoesc":
+                            case "auto_escape":
+                            case "auto_escaping":
+                            case "auto_esc":
                                 correctName = "autoEsc";
-                               } else {
+                                break;
+                               default:
                                 correctName = null;
                                }
-                            throw new ParseException(
-                                    "Unknown FTL header parameter: " + 
key.image
-                                    + (correctName == null ? "" : ". You may 
meant: " + correctName),
-                                    template, key);
+
+                               if (correctName != null) {
+                                   sb.append("\nThe correct name is: 
").append(correctName);
+                               }
+
+                            throw new ParseException(sb.toString(), template, 
key);
                         }
                     }
                 }


Reply via email to