You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/07/08 20:35:20 UTC

incubator-freemarker git commit: Continued FM2 to FM3 converter...

Repository: incubator-freemarker
Updated Branches:
  refs/heads/3 344b95411 -> 22d3ef2e0


Continued FM2 to FM3 converter...


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

Branch: refs/heads/3
Commit: 22d3ef2e089de184e589a288bbe4fa9031487ec0
Parents: 344b954
Author: ddekany <dd...@apache.org>
Authored: Sat Jul 8 22:35:03 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Sat Jul 8 22:35:03 2017 +0200

----------------------------------------------------------------------
 .../core/FM2ASTToFM3SourceConverter.java        | 271 +++++++++++++++----
 .../core/UnexpectedNodeContentException.java    |   5 +-
 .../apache/freemarker/converter/Converter.java  |  39 +--
 .../converter/ConverterException.java           |  61 ++++-
 .../UnconvertableLegacyFeatureException.java    |  43 +++
 .../converter/FM2ToFM3ConverterTest.java        |  58 +++-
 .../converter/GenericConverterTest.java         |  20 ++
 7 files changed, 418 insertions(+), 79 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/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 de31532..6ff6958 100644
--- a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
+++ b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
@@ -20,6 +20,7 @@
 package freemarker.core;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -32,6 +33,7 @@ import org.apache.freemarker.converter.ConversionMarkers;
 import org.apache.freemarker.converter.ConversionMarkers.Type;
 import org.apache.freemarker.converter.ConverterException;
 import org.apache.freemarker.converter.ConverterUtils;
+import org.apache.freemarker.converter.UnconvertableLegacyFeatureException;
 import org.apache.freemarker.core.NamingConvention;
 import org.apache.freemarker.core.util.FTLUtil;
 import org.apache.freemarker.core.util._ClassUtil;
@@ -214,11 +216,11 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private String convertFtlHeaderParamName(String name) throws ConverterException {
-        name = name.indexOf('_') == -1 ? name : ConverterUtils.snakeCaseToCamelCase(name);
-        if (name.equals("attributes")) {
-            name = "customSettings";
+        String converted = name.indexOf('_') == -1 ? name : ConverterUtils.snakeCaseToCamelCase(name);
+        if (converted.equals("attributes")) {
+            converted = "customSettings";
         }
-        return name;
+        return converted;
     }
 
     private void printNode(TemplateObject node) throws ConverterException {
@@ -257,20 +259,25 @@ public class FM2ASTToFM3SourceConverter {
 
         boolean isNoParseBlock = src.startsWith(tagBeginChar + "#no", startPos);
         if (isNoParseBlock) {
-            printCoreDirStartTagParameterless(node, "noParse");
+            printDirStartTagNoParamsHasNested(node, "noParse");
         }
         print(getOnlyParam(node, ParameterRole.CONTENT, String.class));
         if (isNoParseBlock) {
-            printCoreDirEndTag(node, NO_PARSE_FM_2_TAG_NAMES, "noParse");
+            printDirEndTag(node, NO_PARSE_FM_2_TAG_NAMES, "noParse");
         }
     }
 
     private static final ImmutableList<String> NO_PARSE_FM_2_TAG_NAMES = ImmutableList.of("noparse", "noParse");
 
-    private void printComment(Comment node) throws UnexpectedNodeContentException {
+    private void printComment(Comment node) throws UnexpectedNodeContentException, UnconvertableLegacyFeatureException {
         print(tagBeginChar);
         print("#--");
-        print(getOnlyParam(node, ParameterRole.CONTENT, String.class));
+        String content = getOnlyParam(node, ParameterRole.CONTENT, String.class);
+        if (content.indexOf("-->") != -1) {
+            throw new UnconvertableLegacyFeatureException("You can't have a \"-->\" inside a comment.",
+                    node.getBeginLine(), node.getBeginColumn());
+        }
+        print(content);
         print("--");
         print(tagEndChar);
     }
@@ -385,13 +392,149 @@ public class FM2ASTToFM3SourceConverter {
             printDirBreak((BreakInstruction) node);
         } else if (node instanceof TrimInstruction) {
             printDirTOrNtOrLtOrRt((TrimInstruction) node);
+        } else if (node instanceof PropertySetting) {
+            printDirSetting((PropertySetting) node);
+        } else if (node instanceof StopInstruction) {
+            printDirStop((StopInstruction) node);
+        } else if (node instanceof SwitchBlock) {
+            printDirSwitch((SwitchBlock) node);
+        } else if (node instanceof Case) {
+            printDirCase((Case) node);
+        } else if (node instanceof VisitNode) {
+            printDirVisit((VisitNode) node);
+        } else if (node instanceof RecurseNode) {
+            printDirRecurse((RecurseNode) node);
+        } else if (node instanceof FallbackInstruction) {
+            printDirFallback((FallbackInstruction) node);
         } else {
             throw new ConverterException("Unhandled AST TemplateElement class: " + node.getClass().getName());
         }
     }
 
+    private void printDirFallback(FallbackInstruction node) throws ConverterException {
+        printDirGenericNoParamsNoNested(node, "fallback");
+    }
+
+    private void printDirVisit(VisitNode node) throws ConverterException {
+        printDirVisitLike(node, "visit");
+    }
+
+    private void printDirRecurse(RecurseNode node) throws ConverterException {
+        printDirVisitLike(node, "recurse");
+    }
+
+    private void printDirVisitLike(TemplateElement node, String tagName) throws ConverterException {
+        assertParamCount(node, 2);
+
+        printStartTagPartBeforeParams(node, tagName);
+
+        Expression lastParam;
+
+        Expression nodeExp = getParam(node, 0, ParameterRole.NODE, Expression.class);
+        printExp(nodeExp);
+        lastParam = nodeExp;
+
+        Expression ns = getParam(node, 1, ParameterRole.NAMESPACE, Expression.class);
+        if (ns != null) {
+            printSeparatorAndWSAndExpComments(getEndPositionExclusive(lastParam), "using");
+            printExp(ns);
+            lastParam = ns;
+        }
+
+        printStartTagEnd(node, lastParam, false);
+    }
+
+    private void printDirCase(Case node) throws ConverterException {
+        assertParamCount(node, 2);
+
+        String tagName;
+        Integer subtype = getParam(node, 1, ParameterRole.AST_NODE_SUBTYPE, Integer.class);
+        if (subtype == Case.TYPE_CASE) {
+            tagName = "case";
+        } else if (subtype == Case.TYPE_DEFAULT) {
+            tagName = "default";
+        } else {
+            throw new UnexpectedNodeContentException(node, "Unsupported subtype {}", subtype);
+        }
+
+        int pos = printStartTagPartBeforeParams(node, tagName);
+
+        Expression value = getParam(node, 0, ParameterRole.CONDITION, Expression.class);
+        if (value != null) {
+            printExp(value);
+            pos = getEndPositionExclusive(value);
+        }
+
+        printStartTagEnd(node, pos, false);
+
+        printChildElements(node);
+
+        // Element end tag is always omitted
+    }
+
+    private void printDirSwitch(SwitchBlock node) throws ConverterException {
+        assertParamCount(node, 1);
+
+        printStartTagPartBeforeParams(node, "switch");
+
+        Expression param = getOnlyParam(node, ParameterRole.VALUE, Expression.class);
+        printExp(param);
+
+        printStartTagEnd(node, param, false);
+
+        printChildElements(node);
+
+        printDirEndTag(node, "switch");
+    }
+
+    private void printDirStop(StopInstruction node) throws ConverterException {
+        assertParamCount(node, 1);
+
+        int pos = printStartTagPartBeforeParams(node, "stop");
+        Expression message = getParam(node, 0, ParameterRole.MESSAGE, Expression.class);
+        if (message != null) {
+            printExp(message);
+            pos = getEndPositionExclusive(message);
+        }
+        printStartTagEnd(node, pos, false);
+    }
+
+    private void printDirSetting(PropertySetting node) throws ConverterException {
+        assertParamCount(node, 2);
+
+        int pos = printStartTagPartBeforeParams(node, "setting");
+
+        print(FTLUtil.escapeIdentifier(convertSettingName(
+                getParam(node, 0, ParameterRole.ITEM_KEY, String.class),
+                node)));
+        pos = getPositionAfterIdentifier(pos);
+
+        pos = printSeparatorAndWSAndExpComments(pos, "=");
+
+        Expression paramValue = getParam(node, 1, ParameterRole.ITEM_VALUE, Expression.class);
+        printExp(paramValue);
+
+        printStartTagEnd(node, paramValue, false);
+    }
+
+    private String convertSettingName(String name, TemplateObject node) throws ConverterException {
+        String converted = name.indexOf('_') == -1 ? name : ConverterUtils.snakeCaseToCamelCase(name);
+
+        if (converted.equals("classicCompatible")) {
+            throw new UnconvertableLegacyFeatureException("There \"classicCompatible\" setting doesn't exist in "
+                    + "FreeMarker 3. You have to remove it manually before conversion.",
+                    node.getBeginLine(), node.getBeginColumn());
+        }
+
+        if (!Arrays.asList(PropertySetting.SETTING_NAMES).contains(converted)) {
+            throw new ConverterException("Couldn't map \"" + name + "\" to a valid FreeMarker 3 setting name "
+                    + "(tried: " + converted + ")");
+        }
+        return converted;
+    }
+
     private void printDirTOrNtOrLtOrRt(TrimInstruction node) throws ConverterException {
-        int subtype= getOnlyParam(node, ParameterRole.AST_NODE_SUBTYPE, Integer.class);
+        int subtype = getOnlyParam(node, ParameterRole.AST_NODE_SUBTYPE, Integer.class);
         String tagName;
         if (subtype == TrimInstruction.TYPE_T) {
             tagName = "t";
@@ -405,11 +548,11 @@ public class FM2ASTToFM3SourceConverter {
             throw new UnexpectedNodeContentException(node, "Unhandled subtype {}.", subtype);
         }
 
-        printCoreDirStartTagParameterless(node, tagName);
+        printDirStartTagNoParamsNoNested(node, tagName);
     }
 
     private void printDirNested(BodyInstruction node) throws ConverterException {
-        int pos = printCoreDirStartTagBeforeParams(node, "nested");
+        int pos = printStartTagPartBeforeParams(node, "nested");
         int paramCnt = node.getParameterCount();
         for (int paramIdx = 0; paramIdx < paramCnt; paramIdx++) {
             Expression passedValue = getParam(node, paramIdx, ParameterRole.PASSED_VALUE, Expression.class);
@@ -423,11 +566,11 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private void printDirBreak(BreakInstruction node) throws ConverterException {
-        printCoreDirStartTagParameterless(node, "break");
+        printDirStartTagNoParamsNoNested(node, "break");
     }
 
     private void printDirItems(Items node) throws ConverterException {
-        int pos = printCoreDirStartTagBeforeParams(node, "items");
+        int pos = printStartTagPartBeforeParams(node, "items");
         pos = printSeparatorAndWSAndExpComments(pos, "as");
 
         int paramCnt = node.getParameterCount();
@@ -447,7 +590,7 @@ public class FM2ASTToFM3SourceConverter {
 
         printChildElements(node);
 
-        printCoreDirEndTag(node, "items");
+        printDirEndTag(node, "items");
     }
 
     private void printDirListElseContainer(ListElseContainer node) throws ConverterException {
@@ -455,25 +598,25 @@ public class FM2ASTToFM3SourceConverter {
 
         printDirListOrForeach((IteratorBlock) node.getChild(0), false);
         printDirElseOfList((ElseOfList) node.getChild(1));
-        printCoreDirEndTag(node, "list");
+        printDirEndTag(node, "list");
     }
 
     private void printDirElseOfList(ElseOfList node) throws ConverterException {
-        printCoreDirStartTagParameterless(node, "else");
+        printDirStartTagNoParamsHasNested(node, "else");
         printChildElements(node);
     }
 
     private void printDirSep(Sep node) throws ConverterException {
-        printCoreDirStartTagParameterless(node, "sep");
+        printDirStartTagNoParamsHasNested(node, "sep");
         printChildElements(node);
-        printCoreDirEndTag(node, Collections.singleton("sep"), "sep", true);
+        printDirEndTag(node, Collections.singleton("sep"), "sep", true);
     }
 
     private void printDirListOrForeach(IteratorBlock node, boolean printEndTag) throws ConverterException {
         int paramCount = node.getParameterCount();
         assertNodeContent(paramCount <= 3, node, "ParameterCount <= 3 was expected");
 
-        int pos = printCoreDirStartTagBeforeParams(node, "list");
+        int pos = printStartTagPartBeforeParams(node, "list");
 
         Expression listSource = getParam(node, 0, ParameterRole.LIST_SOURCE, Expression.class);
         // To be future proof, we don't assume that the parameter count of list don't include the null parameters.
@@ -550,7 +693,7 @@ public class FM2ASTToFM3SourceConverter {
         printChildElements(node);
 
         if (printEndTag) {
-            printCoreDirEndTag(node, LIST_FM_2_TAG_NAMES, "list", false);
+            printDirEndTag(node, LIST_FM_2_TAG_NAMES, "list", false);
         }
     }
 
@@ -559,7 +702,7 @@ public class FM2ASTToFM3SourceConverter {
     private void printDirInclude(Include node) throws ConverterException {
         assertParamCount(node, 4);
 
-        printCoreDirStartTagBeforeParams(node, "include");
+        printStartTagPartBeforeParams(node, "include");
 
         Expression templateName = getParam(node, 0, ParameterRole.TEMPLATE_NAME, Expression.class);
         int templateNameEndPos = getEndPositionExclusive(templateName);
@@ -642,7 +785,7 @@ public class FM2ASTToFM3SourceConverter {
     private void printDirImport(LibraryLoad node) throws ConverterException {
         assertParamCount(node, 2);
 
-        printCoreDirStartTagBeforeParams(node, "import");
+        printStartTagPartBeforeParams(node, "import");
 
         Expression templateName = getParam(node, 0, ParameterRole.TEMPLATE_NAME, Expression.class);
         printExp(templateName);
@@ -656,7 +799,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private void printDirReturn(ReturnInstruction node) throws ConverterException {
-        printCoreDirStartTagBeforeParams(node, "return");
+        printStartTagPartBeforeParams(node, "return");
 
         Expression value = getOnlyParam(node, ParameterRole.VALUE, Expression.class);
         printExp(value);
@@ -664,11 +807,11 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private void printDirFlush(FlushInstruction node) throws ConverterException {
-        printDirGenericParameterlessWithoutNestedContent(node, "flush");
+        printDirGenericNoParamsNoNested(node, "flush");
     }
 
     private void printDirNoEscape(NoEscapeBlock node) throws ConverterException {
-        printDirGenericParameterlessWithNestedContent(node, NO_ESCAPE_FM_2_TAG_NAMES, "noEscape");
+        printDirGenericNoParamsHasNested(node, NO_ESCAPE_FM_2_TAG_NAMES, "noEscape");
     }
 
     private static final ImmutableList<String> NO_ESCAPE_FM_2_TAG_NAMES = ImmutableList.of("noescape", "noEscape");
@@ -676,7 +819,7 @@ public class FM2ASTToFM3SourceConverter {
     private void printDirEscape(EscapeBlock node) throws ConverterException {
         assertParamCount(node, 2);
 
-        int pos = printCoreDirStartTagBeforeParams(node, "escape");
+        int pos = printStartTagPartBeforeParams(node, "escape");
 
         pos = getPositionAfterIdentifier(pos);
         print(FTLUtil.escapeIdentifier(getParam(node, 0, ParameterRole.PLACEHOLDER_VARIABLE, String.class)));
@@ -689,61 +832,61 @@ public class FM2ASTToFM3SourceConverter {
 
         printChildElements(node);
 
-        printCoreDirEndTag(node, "escape");
+        printDirEndTag(node, "escape");
     }
 
     private void printDirCompress(CompressedBlock node) throws ConverterException {
-        printDirGenericParameterlessWithNestedContent(node, "compress");
+        printDirGenericNoParamsHasNested(node, "compress");
     }
 
     private void printDirAutoEsc(AutoEscBlock node) throws ConverterException {
-        printDirGenericParameterlessWithNestedContent(node, AUTO_ESC_FM_2_TAG_NAMES, "autoEsc");
+        printDirGenericNoParamsHasNested(node, AUTO_ESC_FM_2_TAG_NAMES, "autoEsc");
     }
 
     private static final ImmutableList<String> AUTO_ESC_FM_2_TAG_NAMES = ImmutableList.of("autoesc", "autoEsc");
 
     private void printDirNoAutoEsc(NoAutoEscBlock node) throws ConverterException {
-        printDirGenericParameterlessWithNestedContent(node, NO_AUTO_ESC_FM_2_TAG_NAMES, "noAutoEsc");
+        printDirGenericNoParamsHasNested(node, NO_AUTO_ESC_FM_2_TAG_NAMES, "noAutoEsc");
     }
 
     private static final ImmutableList<String> NO_AUTO_ESC_FM_2_TAG_NAMES = ImmutableList.of("noautoesc", "noAutoEsc");
 
-    private void printDirGenericParameterlessWithNestedContent(TemplateElement node, String fm3TagName)
+    private void printDirGenericNoParamsHasNested(TemplateElement node, String fm3TagName)
             throws ConverterException {
-        printDirGenericParameterlessWithNestedContent(node, Collections.singleton(fm3TagName), fm3TagName);
+        printDirGenericNoParamsHasNested(node, Collections.singleton(fm3TagName), fm3TagName);
     }
 
-    private void printDirGenericParameterlessWithNestedContent(TemplateElement node,
+    private void printDirGenericNoParamsHasNested(TemplateElement node,
             Collection<String> fm2TagName, String fm3TagName)
             throws ConverterException {
         assertParamCount(node, 0);
 
-        printCoreDirStartTagParameterless(node, fm3TagName);
+        printDirStartTagNoParamsHasNested(node, fm3TagName);
         printChildElements(node);
-        printCoreDirEndTag(node, fm2TagName, fm3TagName);
+        printDirEndTag(node, fm2TagName, fm3TagName);
     }
 
-    private void printDirGenericParameterlessWithoutNestedContent(TemplateElement node, String name)
+    private void printDirGenericNoParamsNoNested(TemplateElement node, String name)
             throws ConverterException {
         assertParamCount(node, 0);
-        printCoreDirStartTagParameterless(node, name);
+        printDirStartTagNoParamsNoNested(node, name);
     }
 
     private void printDirAttemptRecover(AttemptBlock node) throws ConverterException {
         assertParamCount(node, 1); // 1: The recovery block
 
-        printCoreDirStartTagParameterless(node, "attempt");
+        printDirStartTagNoParamsHasNested(node, "attempt");
 
         printNode(node.getChild(0));
         assertNodeContent(node.getChild(1) instanceof RecoveryBlock, node, "child[1] should be #recover");
 
         RecoveryBlock recoverDir = getOnlyParam(node, ParameterRole.ERROR_HANDLER, RecoveryBlock.class);
-        printCoreDirStartTagParameterless(recoverDir, "recover");
+        printDirStartTagNoParamsHasNested(recoverDir, "recover");
 
         printChildElements(recoverDir);
 
         // In FM2 this could be </#recover> as well, but we normalize it
-        printCoreDirEndTag(node, ATTEMPT_RECOVER_FM_2_TAG_NAMES, "attempt", false);
+        printDirEndTag(node, ATTEMPT_RECOVER_FM_2_TAG_NAMES, "attempt", false);
     }
 
     private static final ImmutableList<String> ATTEMPT_RECOVER_FM_2_TAG_NAMES = ImmutableList.of("attempt", "recover");
@@ -792,7 +935,7 @@ public class FM2ASTToFM3SourceConverter {
 
         printChildElements(node);
 
-        printCoreDirEndTag(node, getAssignmentDirTagName(node, 1));
+        printDirEndTag(node, getAssignmentDirTagName(node, 1));
     }
 
     private void printDirAssignmentCommonTagAfterLastAssignmentExp(TemplateElement node, int nsParamIdx, int pos)
@@ -812,7 +955,7 @@ public class FM2ASTToFM3SourceConverter {
 
     private int printDirAssignmentCommonTagTillAssignmentExp(TemplateElement node, int scopeParamIdx)
             throws ConverterException {
-        return printCoreDirStartTagBeforeParams(node, getAssignmentDirTagName(node, scopeParamIdx));
+        return printStartTagPartBeforeParams(node, getAssignmentDirTagName(node, scopeParamIdx));
     }
 
     private String getAssignmentDirTagName(TemplateElement node, int scopeParamIdx)
@@ -868,7 +1011,7 @@ public class FM2ASTToFM3SourceConverter {
             throw new UnexpectedNodeContentException(node, "Unhandled node subtype: {}", subtype);
         }
 
-        int pos = printCoreDirStartTagBeforeParams(node, tagName);
+        int pos = printStartTagPartBeforeParams(node, tagName);
 
         String assignedName = getParam(node, 0, ParameterRole.ASSIGNMENT_TARGET, String.class);
         print(FTLUtil.escapeIdentifier(assignedName));
@@ -935,7 +1078,7 @@ public class FM2ASTToFM3SourceConverter {
 
         printChildElements(node);
 
-        printCoreDirEndTag(node, tagName);
+        printDirEndTag(node, tagName);
     }
 
     private void printDirCustom(UnifiedCall node) throws ConverterException {
@@ -1049,24 +1192,24 @@ public class FM2ASTToFM3SourceConverter {
         }
 
         if (conditionExp != null) {
-            printCoreDirStartTagBeforeParams(node, tagName);
+            printStartTagPartBeforeParams(node, tagName);
             printNode(conditionExp);
             printStartTagEnd(node, conditionExp, true);
         } else {
-            printCoreDirStartTagParameterless(node, tagName);
+            printDirStartTagNoParamsHasNested(node, tagName);
         }
 
         printChildElements(node);
 
         if (!(node.getParentElement() instanceof IfBlock)) {
-            printCoreDirEndTag(node, "if");
+            printDirEndTag(node, "if");
         }
     }
 
     private void printDirIfElseElseIfContainer(IfBlock node) throws ConverterException {
         printChildElements(node);
 
-        printCoreDirEndTag(node, "if");
+        printDirEndTag(node, "if");
     }
 
     /**
@@ -1541,7 +1684,7 @@ public class FM2ASTToFM3SourceConverter {
      *
      * @return The position in the source after the printed part
      */
-    private int printCoreDirStartTagBeforeParams(TemplateElement node, String fm3TagName)
+    private int printStartTagPartBeforeParams(TemplateElement node, String fm3TagName)
             throws ConverterException {
         print(tagBeginChar);
         print('#');
@@ -1549,18 +1692,28 @@ public class FM2ASTToFM3SourceConverter {
         return printWSAndExpComments(getPositionAfterTagName(node));
     }
 
-    private int printCoreDirStartTagParameterless(TemplateElement node, String fm3TagName)
+    private int printDirStartTagNoParamsNoNested(TemplateElement node, String fm3TagName)
             throws ConverterException {
-        int pos = printCoreDirStartTagBeforeParams(node, fm3TagName);
-        printStartTagEnd(node, pos, true);
+        return printDirStartTagNoParams(node, fm3TagName, false);
+    }
+
+    private int printDirStartTagNoParamsHasNested(TemplateElement node, String fm3TagName)
+            throws ConverterException {
+        return printDirStartTagNoParams(node, fm3TagName, true);
+    }
+
+    private int printDirStartTagNoParams(TemplateElement node, String fm3TagName, boolean removeSlash)
+            throws ConverterException {
+        int pos = printStartTagPartBeforeParams(node, fm3TagName);
+        printStartTagEnd(node, pos, removeSlash);
         return pos + 1;
     }
 
-    private void printCoreDirEndTag(TemplateElement node, String tagName) throws UnexpectedNodeContentException {
-        printCoreDirEndTag(node, Collections.singleton(tagName), tagName);
+    private void printDirEndTag(TemplateElement node, String tagName) throws UnexpectedNodeContentException {
+        printDirEndTag(node, Collections.singleton(tagName), tagName);
     }
 
-    private void printCoreDirEndTag(TemplateElement node, Collection<String> fm2TagName, String fm3TagName) throws
+    private void printDirEndTag(TemplateElement node, Collection<String> fm2TagName, String fm3TagName) throws
             UnexpectedNodeContentException {
         if (fm2TagName.size() == 0) {
             throw new IllegalArgumentException("You must specify at least 1 FM2 tag names");
@@ -1570,10 +1723,10 @@ public class FM2ASTToFM3SourceConverter {
                     "You must specify multiple FM2 tag names when the FM3 tag name ("
                     + fm3TagName + ") contains upper case letters");
         }
-        printCoreDirEndTag(node, fm2TagName, fm3TagName, false);
+        printDirEndTag(node, fm2TagName, fm3TagName, false);
     }
 
-    private void printCoreDirEndTag(TemplateElement node, Collection<String> fm2TagNames, String fm3TagName,
+    private void printDirEndTag(TemplateElement node, Collection<String> fm2TagNames, String fm3TagName,
             boolean optional)
             throws UnexpectedNodeContentException {
         int tagEndPos = getEndPositionInclusive(node);
@@ -1661,10 +1814,10 @@ public class FM2ASTToFM3SourceConverter {
      */
     private int printStartTagEnd(TemplateElement node, Expression lastParam, boolean trimSlash)
             throws ConverterException {
+        _NullArgumentException.check("lastParam", lastParam);
         return printStartTagEnd(
                 node,
-                lastParam == null ? getPositionAfterTagName(node)
-                        : getPosition(lastParam.getEndColumn() + 1, lastParam.getEndLine()),
+                getPosition(lastParam.getEndColumn() + 1, lastParam.getEndLine()),
                 trimSlash);
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java b/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
index c8bfe73..f7c7408 100644
--- a/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
+++ b/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
@@ -25,8 +25,9 @@ import org.apache.freemarker.core.util._StringUtil;
 public class UnexpectedNodeContentException extends ConverterException {
     public UnexpectedNodeContentException(TemplateObject node, String errorMessage, Object msgParam) {
         super("Unexpected AST content for " + _StringUtil.jQuote(node.getNodeTypeSymbol()) + " node (class: "
-                + node.getClass().getName() + ") " + node.getStartLocation() + ":\n"
-                + renderMessage(errorMessage, msgParam));
+                + node.getClass().getName() + "):\n"
+                + renderMessage(errorMessage, msgParam),
+                node.getBeginLine(), node.getBeginColumn());
     }
 
     private static String renderMessage(String errorMessage, Object msgParam) {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/freemarker-converter/src/main/java/org/apache/freemarker/converter/Converter.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/org/apache/freemarker/converter/Converter.java b/freemarker-converter/src/main/java/org/apache/freemarker/converter/Converter.java
index d35a34d..2b3d8eb 100644
--- a/freemarker-converter/src/main/java/org/apache/freemarker/converter/Converter.java
+++ b/freemarker-converter/src/main/java/org/apache/freemarker/converter/Converter.java
@@ -42,6 +42,8 @@ public abstract class Converter {
     public static final String PROPERTY_NAME_DESTINATION_DIRECTORY = "destinationDirectory";
     public static final String CONVERSION_MARKERS_FILE_NAME = "__conversion-markers.txt";
 
+    public static ThreadLocal<FileConversionContext> FILE_CONVERSION_CONTEXT_TLS = new ThreadLocal<>();
+
     private static final Logger LOG = LoggerFactory.getLogger(Converter.class);
 
     private File source;
@@ -148,29 +150,34 @@ public abstract class Converter {
             throw new ConverterException("Failed to open file for reading: " + src, e);
         }
         try {
-            LOG.debug("Converting file: {}", src);
-            FileConversionContext ctx = null;
             try {
-                ctx = new FileConversionContext(srcStream, src, dstDir);
-                convertFile(ctx);
-                storeConversionMarkers(ctx.getConversionMarkers(), ctx);
-            } catch (IOException e) {
-                throw new ConverterException("I/O exception while converting " + _StringUtil.jQuote(src) + ".", e);
-            } finally {
+                LOG.debug("Converting file: {}", src);
+                FileConversionContext ctx = null;
                 try {
-                    if (ctx != null && ctx.outputStream != null) {
-                        ctx.outputStream.close();
+                    ctx = new FileConversionContext(srcStream, src, dstDir);
+                    FILE_CONVERSION_CONTEXT_TLS.set(ctx);
+                    convertFile(ctx);
+                    storeConversionMarkers(ctx.getConversionMarkers(), ctx);
+                } catch (IOException e) {
+                    throw new ConverterException("I/O exception while converting " + _StringUtil.jQuote(src) + ".", e);
+                } finally {
+                    try {
+                        if (ctx != null && ctx.outputStream != null) {
+                            ctx.outputStream.close();
+                        }
+                    } catch (IOException e) {
+                        throw new ConverterException("Failed to close destination file", e);
                     }
+                }
+            } finally {
+                try {
+                    srcStream.close();
                 } catch (IOException e) {
-                    throw new ConverterException("Failed to close destination file", e);
+                    throw new ConverterException("Failed to close file: " + src, e);
                 }
             }
         } finally {
-            try {
-                srcStream.close();
-            } catch (IOException e) {
-                throw new ConverterException("Failed to close file: " + src, e);
-            }
+            FILE_CONVERSION_CONTEXT_TLS.remove();
         }
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterException.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterException.java b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterException.java
index 4a6533b..d474805 100644
--- a/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterException.java
+++ b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConverterException.java
@@ -19,14 +19,73 @@
 
 package org.apache.freemarker.converter;
 
+import org.apache.freemarker.core.util._NullArgumentException;
+
 public class ConverterException extends Exception {
 
+    private final Integer row;
+    private final Integer column;
+
     public ConverterException(String message) {
         this(message, null);
     }
 
     public ConverterException(String message, Throwable cause) {
-        super(message, cause);
+        this(message, null, null, cause);
+    }
+
+    /**
+     * See {@link #ConverterException(String, Integer, Integer, Throwable)}
+     */
+    public ConverterException(String message, Integer row, Integer column) {
+        this(message, row, column, null);
+    }
+
+    /**
+     * @param row The 1-based row in the source file, or {@code null}.
+     * @param column The 1-based column in the source file, or {@code null}.
+     */
+    public ConverterException(String message, Integer row, Integer column, Throwable cause) {
+        super(addLocationToMessage(message, row, column), cause);
+        this.row = row;
+        this.column = column;
+    }
+
+    private static String addLocationToMessage(String message, Integer row, Integer column) {
+        _NullArgumentException.check("message", message);
+
+        StringBuilder sb = new StringBuilder();
+
+        Converter.FileConversionContext ctx = Converter.FILE_CONVERSION_CONTEXT_TLS.get();
+        if (ctx != null || row != null) {
+            sb.append("At ");
+            if (ctx != null) {
+                sb.append(ctx.getSourceFile()).append(':');
+            }
+            if (row != null) {
+                sb.append(row).append(':');
+                if (column != null) {
+                    sb.append(column).append(':');
+                }
+            }
+            sb.append(" ");
+        }
+
+        sb.append(message);
+        return sb.toString();
     }
 
+    /**
+     * The 1-based row in the source file, or {@code null}.
+     */
+    public Integer getRow() {
+        return row;
+    }
+
+    /**
+     * The 1-based column in the source file, or {@code null}.
+     */
+    public Integer getColumn() {
+        return column;
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/freemarker-converter/src/main/java/org/apache/freemarker/converter/UnconvertableLegacyFeatureException.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/org/apache/freemarker/converter/UnconvertableLegacyFeatureException.java b/freemarker-converter/src/main/java/org/apache/freemarker/converter/UnconvertableLegacyFeatureException.java
new file mode 100644
index 0000000..c3a4d6e
--- /dev/null
+++ b/freemarker-converter/src/main/java/org/apache/freemarker/converter/UnconvertableLegacyFeatureException.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.freemarker.converter;
+
+/**
+ * The legacy feature has no equivalent in the target format.
+ */
+public class UnconvertableLegacyFeatureException extends ConverterException {
+
+    /**
+     * @param row 1-based
+     * @param column 1-based
+     */
+    public UnconvertableLegacyFeatureException(String message, int row, int column) {
+        this(message, row, column, null);
+    }
+
+    /**
+     * @param row 1-based
+     * @param column 1-based
+     */
+    public UnconvertableLegacyFeatureException(String message, int row, int column, Throwable cause) {
+        super(message, row, column, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
index 1243251..a76350c 100644
--- a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
+++ b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
@@ -30,6 +30,7 @@ import java.util.Properties;
 import org.apache.commons.io.FileUtils;
 import org.apache.freemarker.converter.ConverterException;
 import org.apache.freemarker.converter.FM2ToFM3Converter;
+import org.apache.freemarker.converter.UnconvertableLegacyFeatureException;
 import org.freemarker.converter.test.ConverterTest;
 import org.junit.Test;
 
@@ -308,9 +309,64 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
         assertConvertedSame("a<#t>\nb");
         assertConvertedSame("<#t><#nt><#lt><#rt>");
         assertConvertedSame("<#t ><#nt ><#lt ><#rt >");
-        assertConverted("<#t><#nt><#lt><#rt>", "<#t /><#nt /><#lt /><#rt />");
+        assertConvertedSame("<#t><#nt><#lt><#rt>");
 
         assertConvertedSame("<#ftl stripText='true'>\n\n<#macro m>\nx\n</#macro>\n");
+
+        assertConvertedSame("<#setting <#--1--> numberFormat <#--2--> = <#--3--> '0.0' <#--4-->>");
+        assertConverted("<#setting numberFormat='0.0' />", "<#setting number_format='0.0' />");
+        try {
+            convert("x<#setting classic_compatible=true>");
+            fail();
+        } catch (UnconvertableLegacyFeatureException e) {
+            assertEquals(1, (Object) e.getRow());
+            assertEquals(2, (Object) e.getColumn());
+        }
+
+        assertConvertedSame("<#stop>");
+        assertConvertedSame("<#stop />");
+        assertConvertedSame("<#stop 'Reason'>");
+        assertConvertedSame("<#stop <#--1--> 'Reason' <#--2-->>");
+
+        assertConvertedSame(""
+                + "<#switch x>\n"
+                + "  <#--1-->\n"
+                + "  <#case 1>one<#break>\n"
+                + "  <#--2-->\n"
+                + "  <#case 3>one<#break />\n"
+                + "  <#case 3>fall through<#case 4>three<#break>\n"
+                + "  <#default>def\n"
+                + "</#switch>");
+        assertConvertedSame(""
+                + "<#switch x>\n"
+                + "  <#--1-->\n"
+                + "</#switch>");
+        assertConvertedSame("<#switch x> </#switch>");
+        assertConvertedSame("<#switch x><#-- Empty --></#switch>");
+        assertConverted("<#switch x> <#case 2> </#switch>", "<#switch x> <#case 2> </#switch>");
+
+        assertConvertedSame("<#visit node>");
+        assertConvertedSame("<#visit <#--1--> node <#--2-->>");
+        assertConvertedSame("<#visit node using ns>");
+        assertConvertedSame("<#visit node <#--1--> using <#--2--> ns <#--3-->>");
+        assertConvertedSame("<#recurse node>");
+        assertConvertedSame("<#recurse <#--1--> node <#--2-->>");
+        assertConvertedSame("<#recurse node using ns>");
+        assertConvertedSame("<#recurse node <#--1--> using <#--2--> ns <#--3-->>");
+        assertConvertedSame("<#macro m><#fallback></#macro>");
+        assertConvertedSame("<#macro m><#fallback /></#macro>");
+    }
+
+    @Test
+    public void testLegacyDirectives() throws IOException, ConverterException {
+        assertConverted("<#--<#bar>-->", "<#comment><#bar></#comment>");
+        try {
+            convert("x<#comment>--></#comment>");
+            fail();
+        } catch (UnconvertableLegacyFeatureException e) {
+            assertEquals(1, (Object) e.getRow());
+            assertEquals(2, (Object) e.getColumn());
+        }
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/22d3ef2e/freemarker-converter/src/test/java/org/freemarker/converter/GenericConverterTest.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/test/java/org/freemarker/converter/GenericConverterTest.java b/freemarker-converter/src/test/java/org/freemarker/converter/GenericConverterTest.java
index 23fae4b..7fcee1a 100644
--- a/freemarker-converter/src/test/java/org/freemarker/converter/GenericConverterTest.java
+++ b/freemarker-converter/src/test/java/org/freemarker/converter/GenericConverterTest.java
@@ -197,12 +197,28 @@ public class GenericConverterTest extends ConverterTest {
         assertTrue(markersFile.exists());
     }
 
+    @Test
+    public void testLocationInException() throws IOException, ConverterException {
+        write(new File(srcDir, "error.txt"), "x[trigger error]", UTF_8);
+
+        ToUpperCaseConverter converter = new ToUpperCaseConverter();
+        converter.setSource(srcDir);
+        converter.setDestinationDirectory(dstDir);
+        try {
+            converter.execute();
+            fail();
+        } catch (ConverterException e) {
+            assertThat(e.getMessage(), containsString("error.txt:1:2: Error message"));
+        }
+    }
+
     public static class ToUpperCaseConverter extends Converter {
 
         @Override
         protected void convertFile(FileConversionContext ctx) throws ConverterException, IOException {
             String content = IOUtils.toString(ctx.getSourceStream(), StandardCharsets.UTF_8);
             ctx.setDestinationFileName(ctx.getSourceFileName() + ".uc");
+
             if (content.contains("[trigger warn]")) {
                 ctx.getConversionMarkers().markInSource(
                         1, 2, ConversionMarkers.Type.WARN, "Warn message");
@@ -211,6 +227,10 @@ public class GenericConverterTest extends ConverterTest {
                 ctx.getConversionMarkers().markInDestination(
                         1, 2, ConversionMarkers.Type.TIP, "Tip message");
             }
+            if (content.contains("[trigger error]")) {
+                throw new ConverterException("Error message", 1, 2);
+            }
+
             IOUtils.write(content.toUpperCase(), ctx.getDestinationStream(), StandardCharsets.UTF_8);
         }