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/05 23:39:33 UTC

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

Repository: incubator-freemarker
Updated Branches:
  refs/heads/3 18e939961 -> 9b31510bb


Continued work on 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/9b31510b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/9b31510b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/9b31510b

Branch: refs/heads/3
Commit: 9b31510bb22eeecb5b8ca550104cd977f851ed6f
Parents: 18e9399
Author: ddekany <dd...@apache.org>
Authored: Thu Jul 6 01:38:41 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Thu Jul 6 01:38:41 2017 +0200

----------------------------------------------------------------------
 .../core/FM2ASTToFM3SourceConverter.java        | 289 +++++++++++++------
 .../converter/ConversionWarnReceiver.java       |  44 +++
 .../apache/freemarker/converter/Converter.java  |  30 +-
 .../freemarker/converter/FM2ToFM3Converter.java |   5 +-
 .../converter/LoggingWarnReceiver.java          |  44 +++
 .../converter/FM2ToFM3ConverterTest.java        |  18 ++
 6 files changed, 332 insertions(+), 98 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/9b31510b/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 e2baaa1..4c2d19c 100644
--- a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
+++ b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
@@ -20,11 +20,14 @@
 package freemarker.core;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.freemarker.converter.ConversionWarnReceiver;
 import org.apache.freemarker.converter.ConverterException;
 import org.apache.freemarker.converter.ConverterUtils;
 import org.apache.freemarker.core.NamingConvention;
@@ -66,6 +69,8 @@ public class FM2ASTToFM3SourceConverter {
 
     private final Template template;
     private final String src;
+    private final ConversionWarnReceiver warnReceiver;
+
     private final StringBuilder out;
     private List<Integer> rowStartPositions;
     private final char tagBeginChar;
@@ -79,11 +84,14 @@ public class FM2ASTToFM3SourceConverter {
     private boolean printNextCustomDirAsFtlDir;
 
     /**
-     * @param fm2Cfg The {@link Configuration} used for parsing; {@link Configuration#getWhitespaceStripping()} must
-     *               return {@code false}.
+     * @param fm2Cfg
+     *         The {@link Configuration} used for parsing; {@link Configuration#getWhitespaceStripping()} must return
+     *         {@code false}.
      */
-    public static Result convert(String templateName, String src, Configuration fm2Cfg) throws ConverterException {
-        return new FM2ASTToFM3SourceConverter(templateName, src, fm2Cfg).convert();
+    public static Result convert(
+            String templateName, String src, Configuration fm2Cfg, ConversionWarnReceiver warnReceiver)
+            throws ConverterException {
+        return new FM2ASTToFM3SourceConverter(templateName, src, fm2Cfg, warnReceiver).convert();
     }
 
     private Result convert() throws ConverterException {
@@ -101,7 +109,8 @@ public class FM2ASTToFM3SourceConverter {
         return new Result(template, outAsString);
     }
 
-    private FM2ASTToFM3SourceConverter(String templateName, String src, Configuration fm2Cfg)
+    private FM2ASTToFM3SourceConverter(
+            String templateName, String src, Configuration fm2Cfg, ConversionWarnReceiver warnReceiver)
             throws ConverterException {
         template = createTemplate(templateName, src, fm2Cfg);
         if (template.getParserConfiguration().getWhitespaceStripping()) {
@@ -111,6 +120,9 @@ public class FM2ASTToFM3SourceConverter {
         _NullArgumentException.check("src", src);
 
         this.src = src;
+
+        this.warnReceiver = warnReceiver;
+
         this.out = new StringBuilder();
         if (template.getActualTagSyntax() == Configuration.SQUARE_BRACKET_TAG_SYNTAX) {
             tagBeginChar = '[';
@@ -182,7 +194,8 @@ public class FM2ASTToFM3SourceConverter {
             FM2ASTToFM3SourceConverter customFtlDirSrcConverter = new FM2ASTToFM3SourceConverter(
                     template.getName(),
                     tagBeginChar + "@ftl" + src.substring(pos, tagEnd) + (hasSlash ? "" : "/") + tagEndChar,
-                    template.getConfiguration());
+                    template.getConfiguration(), warnReceiver
+            );
             customFtlDirSrcConverter.printNextCustomDirAsFtlDir = true;
             String fm3Content = customFtlDirSrcConverter.convert().fm3Content;
             print(hasSlash
@@ -269,17 +282,17 @@ public class FM2ASTToFM3SourceConverter {
     private boolean needsNoParenthesisAsBuiltInLHO(Expression exp) {
         return
                 exp instanceof Identifier
-                || exp instanceof NumberLiteral
-                || exp instanceof BooleanLiteral
-                || exp instanceof StringLiteral
-                || exp instanceof ListLiteral
-                || exp instanceof HashLiteral
-                || exp instanceof ParentheticalExpression
-                || exp instanceof MethodCall
-                || exp instanceof DynamicKeyName
-                || exp instanceof BuiltIn
-                || exp instanceof BuiltinVariable
-                || exp instanceof Dot;
+                        || exp instanceof NumberLiteral
+                        || exp instanceof BooleanLiteral
+                        || exp instanceof StringLiteral
+                        || exp instanceof ListLiteral
+                        || exp instanceof HashLiteral
+                        || exp instanceof ParentheticalExpression
+                        || exp instanceof MethodCall
+                        || exp instanceof DynamicKeyName
+                        || exp instanceof BuiltIn
+                        || exp instanceof BuiltinVariable
+                        || exp instanceof Dot;
     }
 
     private void printDollarInterpolation(DollarVariable node) throws ConverterException {
@@ -324,11 +337,102 @@ public class FM2ASTToFM3SourceConverter {
             printDirReturn((ReturnInstruction) node);
         } else if (node instanceof LibraryLoad) {
             printDirImport((LibraryLoad) node);
+        } else if (node instanceof Include) {
+            printDirInclude((Include) node);
         } else {
             throw new ConverterException("Unhandled AST TemplateElement class: " + node.getClass().getName());
         }
     }
 
+    private void printDirInclude(Include node) throws ConverterException {
+        if (Configuration.getVersion().intValue() != Configuration.VERSION_2_3_26.intValue()) {
+            throw new BugException("Fix things at [broken in 2.3.26] comments; version was: "
+                    + Configuration.getVersion());
+        }
+        // assertParamCount(node, 4); // [broken in 2.3.26]
+
+        printCoreDirStartTagBeforeParams(node, "include");
+
+        Expression templateName = getParam(node, 0, ParameterRole.TEMPLATE_NAME, Expression.class);
+        int templateNameEndPos = getEndPositionExclusive(templateName);
+
+        Expression parseParam = getParam(node, 1, ParameterRole.PARSE_PARAMETER, Expression.class);
+        if (parseParam != null) {
+            warnReceiver.warn(parseParam.getBeginLine(), parseParam.getBeginColumn(),
+                    "The \"parse\" parameter of #include was removed, as it's not supported anymore. Use the "
+                            + "templateConfigurations configuration setting to specify which files are not parsed.");
+
+        }
+
+        Expression encodingParam = getParam(node, 2, ParameterRole.ENCODING_PARAMETER, Expression.class);
+        if (encodingParam != null) {
+            warnReceiver.warn(encodingParam.getBeginLine(), encodingParam.getBeginColumn(),
+                    "The \"encoding\" parameter of #include was removed, as it's not supported anymore. Use the "
+                            + "templateConfigurations configuration setting to specify which files has a different "
+                            + "encoding than the configured default.");
+        }
+
+        // Can't use as parameterCount is [broken in 2.3.26]:
+        // Expression ignoreMissingParam = getParam(node, 3, ParameterRole.IGNORE_MISSING_PARAMETER, Expression.class);
+        Expression ignoreMissingParam = (Expression) node.getParameterValue(3);
+
+        List<Expression> sortedExps =
+                sortExpressionsByPosition(templateName, parseParam, encodingParam, ignoreMissingParam);
+
+        printExp(templateName);
+        String postNameWSOrComment = readAndWSAndExpComments(templateNameEndPos);
+        if (ignoreMissingParam != null || (parseParam == null && encodingParam == null)) {
+            // This will separate us from ignoreMissing=exp, or from the tagEndChar
+            print(postNameWSOrComment);
+        } else {
+            // We only have removed thing after in the src => no need for spacing after us
+            int commentPos = postNameWSOrComment.indexOf("--") - 1;
+            if (commentPos >= 0) {
+                print(rightTrim(postNameWSOrComment));
+            }
+        }
+
+        for (int i = 1; i < sortedExps.size(); i++) {
+            Expression paramExp = sortedExps.get(i);
+            if (paramExp == ignoreMissingParam) {
+                int identifierStartPos = getPositionAfterWSAndExpComments(
+                        getEndPositionExclusive(sortedExps.get(i - 1)));
+                print("ignoreMissing");
+                printSeparatorAndWSAndExpComments(getPositionAfterIdentifier(identifierStartPos), "=");
+                printExp(paramExp);
+
+                String postParamWSOrComment = readAndWSAndExpComments(getEndPositionExclusive(paramExp));
+                if (i == sortedExps.size() - 1) {
+                    // We were the last int the source as well
+                    print(postParamWSOrComment);
+                } else {
+                    int commentPos = postParamWSOrComment.indexOf("--") - 1;
+                    if (commentPos >= 0) {
+                        print(rightTrim(postParamWSOrComment));
+                    }
+                }
+            }
+        }
+
+        print(tagEndChar);
+    }
+
+    private List<Expression> sortExpressionsByPosition(Expression... expressions) {
+        ArrayList<Expression> list = new ArrayList<>(expressions.length);
+        for (Expression expression : expressions) {
+            if (expression != null) {
+                list.add(expression);
+            }
+        }
+        Collections.sort(list, new Comparator<Expression>() {
+            @Override
+            public int compare(Expression o1, Expression o2) {
+                return Integer.compare(getStartPosition(o1), getStartPosition(o2));
+            }
+        });
+        return list;
+    }
+
     private void printDirImport(LibraryLoad node) throws ConverterException {
         assertParamCount(node, 2);
 
@@ -337,12 +441,10 @@ public class FM2ASTToFM3SourceConverter {
         Expression templateName = getParam(node, 0, ParameterRole.TEMPLATE_NAME, Expression.class);
         printExp(templateName);
 
-        int pos = printWSAndExpComments(getEndPositionExclusive(templateName), "as", false);
+        int pos = printSeparatorAndWSAndExpComments(getEndPositionExclusive(templateName), "as");
 
         print(FTLUtil.escapeIdentifier(getParam(node, 1, ParameterRole.NAMESPACE, String.class)));
-        int identifierStartPos = pos;
         pos = getPositionAfterIdentifier(pos);
-        assertNodeContent(pos > identifierStartPos, node, "Can't find namespace variable name");
 
         printStartTagEnd(node, pos, false);
     }
@@ -368,12 +470,10 @@ public class FM2ASTToFM3SourceConverter {
 
         int pos = printCoreDirStartTagBeforeParams(node, "escape");
 
-        int identifierStartPos = pos;
         pos = getPositionAfterIdentifier(pos);
-        assertNodeContent(pos > identifierStartPos, node, "Can't find placeholder variable name");
         print(FTLUtil.escapeIdentifier(getParam(node, 0, ParameterRole.PLACEHOLDER_VARIABLE, String.class)));
 
-        pos = printWSAndExpComments(pos, "as", false);
+        pos = printSeparatorAndWSAndExpComments(pos, "as");
 
         Expression expTemplate = getParam(node, 1, ParameterRole.EXPRESSION_TEMPLATE, Expression.class);
         printExp(expTemplate);
@@ -396,7 +496,8 @@ public class FM2ASTToFM3SourceConverter {
         printDirGenericParameterlessWithNestedContent(node, "noAutoEsc");
     }
 
-    private void printDirGenericParameterlessWithNestedContent(TemplateElement node, String tagName) throws ConverterException {
+    private void printDirGenericParameterlessWithNestedContent(TemplateElement node, String tagName)
+            throws ConverterException {
         assertParamCount(node, 0);
 
         printCoreDirParameterlessStartTag(node, tagName);
@@ -436,7 +537,7 @@ public class FM2ASTToFM3SourceConverter {
             Assignment assignment = (Assignment) node.getChild(childIdx);
             pos = printDirAssignmentCommonExp(assignment, pos);
             if (childIdx != childCnt - 1) {
-                pos = printWSAndExpComments(pos, ",", true);
+                pos = printOptionalSeparatorAndWSAndExpComments(pos, ",");
             }
         }
 
@@ -455,7 +556,7 @@ public class FM2ASTToFM3SourceConverter {
             throws ConverterException {
         Expression ns = getParam(node, nsParamIdx, ParameterRole.NAMESPACE, Expression.class);
         if (ns != null) {
-            pos = printWSAndExpComments(pos, "in", false);
+            printSeparatorAndWSAndExpComments(pos, "in");
             printExp(ns);
             pos = getEndPositionExclusive(ns);
         }
@@ -483,11 +584,9 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private int printDirAssignmentCommonExp(Assignment node, int pos) throws ConverterException {
-        {
-            String target = getParam(node, 0, ParameterRole.ASSIGNMENT_TARGET, String.class);
-            print(FTLUtil.escapeIdentifier(target));
-            pos = getPositionAfterIdentifier(pos, true);
-        }
+        String target = getParam(node, 0, ParameterRole.ASSIGNMENT_TARGET, String.class);
+        print(FTLUtil.escapeIdentifier(target));
+        pos = getPositionAfterAssignmentTargetIdentifier(pos);
 
         pos = printWSAndExpComments(pos);
 
@@ -525,27 +624,19 @@ public class FM2ASTToFM3SourceConverter {
 
         String assignedName = getParam(node, 0, ParameterRole.ASSIGNMENT_TARGET, String.class);
         print(FTLUtil.escapeIdentifier(assignedName));
-        {
-            int lastPos = pos;
-            pos = getPositionAfterIdentifier(pos, true);
-            assertNodeContent(pos > lastPos, node, "Expected target name");
-        }
+        pos = getPositionAfterAssignmentTargetIdentifier(pos);
 
-        pos = printWSAndExpComments(pos, "(", true);
+        pos = printOptionalSeparatorAndWSAndExpComments(pos, "(");
 
         int paramIdx = 1;
         while (node.getParameterRole(paramIdx) == ParameterRole.PARAMETER_NAME) {
             String paramName = getParam(node, paramIdx++, ParameterRole.PARAMETER_NAME, String.class);
             print(FTLUtil.escapeIdentifier(paramName));
-            {
-                int lastPos = pos;
-                pos = getPositionAfterIdentifier(pos);
-                assertNodeContent(pos > lastPos, node, "Expected parameter name");
-            }
+            pos = getPositionAfterIdentifier(pos);
 
             Expression paramDefault = getParam(node, paramIdx++, ParameterRole.PARAMETER_DEFAULT, Expression.class);
             if (paramDefault != null) {
-                printWSAndExpComments(pos, "=", false);
+                printSeparatorAndWSAndExpComments(pos, "=");
                 printExp(paramDefault);
                 pos = getEndPositionExclusive(paramDefault);
             }
@@ -579,12 +670,7 @@ public class FM2ASTToFM3SourceConverter {
         String paramName = getParam(node, paramIdx++, ParameterRole.CATCH_ALL_PARAMETER_NAME, String.class);
         if (paramName != null) {
             print(FTLUtil.escapeIdentifier(paramName));
-            {
-                int lastPos = pos;
-                pos = getPositionAfterIdentifier(pos);
-                assertNodeContent(pos > lastPos, node,
-                        "Expected catch-all parameter name");
-            }
+            pos = getPositionAfterIdentifier(pos);
             pos = printWSAndExpComments(pos);
             assertNodeContent(src.startsWith("...", pos), node,
                     "Expected \"...\" after catch-all parameter name");
@@ -595,7 +681,7 @@ public class FM2ASTToFM3SourceConverter {
         assertNodeContent(paramIdx == paramCnt - 1, node,
                 "Expected AST parameter at index {} to be the last one", paramIdx);
 
-        pos = printWSAndExpComments(pos, ")", true);
+        pos = printOptionalSeparatorAndWSAndExpComments(pos, ")");
         assertNodeContent(src.charAt(pos) == tagEndChar, node, "Tag end not found");
         print(tagEndChar);
 
@@ -636,15 +722,13 @@ public class FM2ASTToFM3SourceConverter {
             Expression argValue = getParam(node, paramIdx + 1, ParameterRole.ARGUMENT_VALUE, Expression.class);
 
             int pos = getEndPositionExclusive(lastPrintedExp);
-            pos = printWSAndExpComments(pos, ",", true);
-            int paramNameStartPos = pos;
+            pos = printOptionalSeparatorAndWSAndExpComments(pos, ",");
             pos = getPositionAfterIdentifier(pos);
-            assertNodeContent(pos > paramNameStartPos, node, "Parameter name in src was empty");
             if (ftlDirMode) {
                 paramName = convertFtlHeaderParamName(paramName);
             }
             print(FTLUtil.escapeIdentifier(paramName));
-            printWSAndExpComments(pos, "=", false);
+            printSeparatorAndWSAndExpComments(pos, "=");
             printExp(argValue);
 
             lastPrintedExp = argValue;
@@ -655,11 +739,7 @@ public class FM2ASTToFM3SourceConverter {
         int pos = getEndPositionExclusive(lastPrintedExp);
         boolean beforeFirstLoopVar = true;
         while (paramIdx < paramCount) {
-            String sep = readWSAndExpComments(pos, beforeFirstLoopVar ? ";" : ",", false);
-            assertNodeContent(sep.length() != 0, node,
-                    "Can't find loop variable separator");
-            printWithConvertedExpComments(sep);
-            pos += sep.length();
+            pos = printSeparatorAndWSAndExpComments(pos, beforeFirstLoopVar ? ";" : ",");
 
             String loopVarName = getParam(node, paramIdx, ParameterRole.TARGET_LOOP_VARIABLE, String.class);
             print(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName));
@@ -831,6 +911,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private static final Map<String, String> COMPARATOR_OP_MAP;
+
     static {
         COMPARATOR_OP_MAP = new HashMap<String, String>();
         // For now we leave FM2 ops as is, but later in many cases they will be replaced.
@@ -860,6 +941,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private static final Map<String, String> AND_OP_MAP;
+
     static {
         AND_OP_MAP = new HashMap<String, String>();
         // For now we leave FM2 ops as is, but later in many cases they will be replaced.
@@ -874,6 +956,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private static final Map<String, String> OR_OP_MAP;
+
     static {
         OR_OP_MAP = new HashMap<String, String>();
         // For now we leave FM2 ops as is, but later in many cases they will be replaced.
@@ -910,7 +993,7 @@ public class FM2ASTToFM3SourceConverter {
     private void printExpBuiltinVariable(BuiltinVariable node) throws ConverterException {
         int startPos = getStartPosition(node);
 
-        int varNameStart = printWSAndExpComments(startPos, ".", false);
+        int varNameStart = printSeparatorAndWSAndExpComments(startPos, ".");
 
         String name = src.substring(varNameStart, getEndPositionExclusive(node));
         print(convertBuiltInVariableName(name));
@@ -929,7 +1012,7 @@ public class FM2ASTToFM3SourceConverter {
         Expression lho = getParam(node, 0, ParameterRole.LEFT_HAND_OPERAND, Expression.class);
         String rho = getParam(node, 1, ParameterRole.RIGHT_HAND_OPERAND, String.class);
         printNode(lho);
-        printWSAndExpComments(getEndPositionExclusive(lho), ".", false);
+        printSeparatorAndWSAndExpComments(getEndPositionExclusive(lho), ".");
         print(FTLUtil.escapeIdentifier(rho));
     }
 
@@ -1098,7 +1181,8 @@ public class FM2ASTToFM3SourceConverter {
         int endPos = getEndPositionInclusive(node);
         boolean foundQuestionMark = false;
         int pos = postLHOPos;
-        scanForRHO: while (pos < endPos) {
+        scanForRHO:
+        while (pos < endPos) {
             char c = src.charAt(pos);
             if (c == '?') {
                 foundQuestionMark = true;
@@ -1203,11 +1287,11 @@ public class FM2ASTToFM3SourceConverter {
         }
     }
 
-    private int printCoreDirStartTagBeforeParams(TemplateElement node, String tagName)
+    private int printCoreDirStartTagBeforeParams(TemplateElement node, String fm3TagName)
             throws ConverterException {
         print(tagBeginChar);
         print('#');
-        print(tagName);
+        print(fm3TagName);
         return printWSAndExpComments(getPositionAfterTagName(node));
     }
 
@@ -1218,10 +1302,10 @@ public class FM2ASTToFM3SourceConverter {
         return pos + 1;
     }
 
-    private void printCoreDirEndTag(TemplateElement node, String tagName) throws UnexpectedNodeContentException {
+    private void printCoreDirEndTag(TemplateElement node, String fm3TagName) throws UnexpectedNodeContentException {
         print(tagBeginChar);
         print("/#");
-        print(tagName);
+        print(fm3TagName);
         printEndTagSkippedTokens(node);
         print(tagEndChar);
     }
@@ -1243,17 +1327,6 @@ public class FM2ASTToFM3SourceConverter {
                 param.getBeginColumn(), param.getBeginLine()));
     }
 
-    private void printWithEnclosedSkippedTokens(
-            String beforeSkippedTokens, String afterSkippedTokens, TemplateObject node)
-            throws ConverterException {
-        print(beforeSkippedTokens);
-        String skippedTokens = getSrcSectionExclEnd(
-                node.getBeginColumn() + beforeSkippedTokens.length(), node.getBeginLine(),
-                node.getEndColumn() - afterSkippedTokens.length() + 1, node.getEndLine());
-        printWithConvertedExpComments(skippedTokens);
-        print(afterSkippedTokens);
-    }
-
     private void printWithParamsTrailingSkippedTokens(
             String afterParams, TemplateObject node, int lastVisualParamIdx) throws
             ConverterException {
@@ -1273,7 +1346,7 @@ public class FM2ASTToFM3SourceConverter {
      * parameter. (This will print the whitespace or comments that isn't visible in the AST.)
      *
      * @return The position of the last character of the start tag. Note that the printed string never includes this
-     *         character.
+     * character.
      */
     private int printStartTagEnd(TemplateElement node, Expression lastParam, boolean trimSlash)
             throws ConverterException {
@@ -1310,7 +1383,8 @@ public class FM2ASTToFM3SourceConverter {
      * Similar to {@link #printStartTagEnd(TemplateElement, Expression, boolean)}, but with explicitly
      * specified scan start position.
      *
-     * @param pos The position where the first skipped character can occur (or the tag end character).
+     * @param pos
+     *         The position where the first skipped character can occur (or the tag end character).
      */
     private int printStartTagEnd(TemplateElement node, int pos, boolean trimSlash)
             throws ConverterException {
@@ -1421,7 +1495,7 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private void assertNodeContent(boolean good, TemplateObject node, String
-        errorMessage) throws UnexpectedNodeContentException {
+            errorMessage) throws UnexpectedNodeContentException {
         assertNodeContent(good, node, errorMessage, null);
     }
 
@@ -1447,8 +1521,10 @@ public class FM2ASTToFM3SourceConverter {
     /**
      * Returns the position of a character in the {@link #src} string.
      *
-     * @param column 1-based column
-     * @param row 1-based row
+     * @param column
+     *         1-based column
+     * @param row
+     *         1-based row
      */
     private int getPosition(int column, int row) {
         if (rowStartPositions == null) {
@@ -1483,7 +1559,8 @@ public class FM2ASTToFM3SourceConverter {
      * @return Position after the whitespace and comments, or the argument position if there were node
      */
     private int getPositionAfterWSAndExpComments(int pos) throws ConverterException {
-        scanForNoWSNoComment: while (pos < src.length()) {
+        scanForNoWSNoComment:
+        while (pos < src.length()) {
             char c = src.charAt(pos);
             if (isExpCommentStart(pos)) {
                 pos += 4; // length of "<#--"
@@ -1507,18 +1584,21 @@ public class FM2ASTToFM3SourceConverter {
         return pos;
     }
 
-    private String readWSAndExpComments(int startPos)
+    private String readAndWSAndExpComments(int startPos)
             throws ConverterException {
         return src.substring(startPos, getPositionAfterWSAndExpComments(startPos));
     }
 
-    private String readWSAndExpComments(int startPos, String separator, boolean separatorOptional)
+    private String readSeparatorAndWSAndExpComments(int startPos, String separator, boolean separatorOptional)
             throws ConverterException {
         int pos = getPositionAfterWSAndExpComments(startPos);
 
         if (pos == src.length() || !src.startsWith(separator, pos)) {
-            // No separator
-            return separatorOptional ? src.substring(startPos, pos) : "";
+            if (!separatorOptional) {
+                throw new ConverterException(
+                        "Expected separator " + _StringUtil.jQuote(separator) + " at position " + pos + ".");
+            }
+            return src.substring(startPos, pos);
         }
         pos += separator.length();
 
@@ -1528,15 +1608,23 @@ public class FM2ASTToFM3SourceConverter {
     }
 
     private int printWSAndExpComments(int pos) throws ConverterException {
-        String sep = readWSAndExpComments(pos);
+        String sep = readAndWSAndExpComments(pos);
         printWithConvertedExpComments(sep);
         pos += sep.length();
         return pos;
     }
 
-    private int printWSAndExpComments(int pos, String separator, boolean sepOptional) throws
-            ConverterException {
-        String sep = readWSAndExpComments(pos, separator, sepOptional);
+    private int printSeparatorAndWSAndExpComments(int pos, String separator) throws ConverterException {
+        return printSeparatorAndWSAndExpComments(pos, separator, false);
+    }
+
+    private int printOptionalSeparatorAndWSAndExpComments(int pos, String separator) throws ConverterException {
+        return printSeparatorAndWSAndExpComments(pos, separator, true);
+    }
+
+    private int printSeparatorAndWSAndExpComments(int pos, String separator, boolean sepOptional)
+            throws ConverterException {
+        String sep = readSeparatorAndWSAndExpComments(pos, separator, sepOptional);
         printWithConvertedExpComments(sep);
         pos += sep.length();
         return pos;
@@ -1546,6 +1634,10 @@ public class FM2ASTToFM3SourceConverter {
         return getPositionAfterIdentifier(startPos, false);
     }
 
+    private int getPositionAfterAssignmentTargetIdentifier(int startPos) throws ConverterException {
+        return getPositionAfterIdentifier(startPos, true);
+    }
+
     private int getPositionAfterIdentifier(int startPos, boolean assignmentTarget) throws ConverterException {
         if (assignmentTarget && looksLikeStringLiteralStart(startPos)) {
             return getPositionAfterStringLiteral(startPos);
@@ -1566,6 +1658,9 @@ public class FM2ASTToFM3SourceConverter {
                     break scanUntilIdentifierEnd;
                 }
             }
+            if (pos == startPos) {
+                throw new ConverterException("Expected an identifier at position " + startPos + ".");
+            }
             return pos;
         }
     }
@@ -1648,4 +1743,16 @@ public class FM2ASTToFM3SourceConverter {
         }
     }
 
+    private String rightTrim(String s) {
+        if (s == null) {
+            return null;
+        }
+
+        int i = s.length() - 1;
+        while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
+            i--;
+        }
+        return i != -1 ? s.substring(0, i + 1) : "";
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/9b31510b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConversionWarnReceiver.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConversionWarnReceiver.java b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConversionWarnReceiver.java
new file mode 100644
index 0000000..f959908
--- /dev/null
+++ b/freemarker-converter/src/main/java/org/apache/freemarker/converter/ConversionWarnReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import java.io.File;
+
+public interface ConversionWarnReceiver {
+
+    /**
+     * Set the file to which the subsequent {@link #warn} calls will refer to.
+     * @param sourceFile
+     */
+    void setSourceFile(File sourceFile);
+
+    /**
+     * @param row
+     *         1-based column in the source file
+     * @param col
+     *         1-based row in the source file
+     * @param message
+     *         Not {@code null}
+     *
+     * @throws IllegalStateException
+     *         If no file was set with {@link #setSourceFile(File)}
+     */
+    void warn(int row, int col, String message);
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/9b31510b/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 0d5ff0b..d9b3ea6 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,7 @@ public abstract class Converter {
 
     private File source;
     private File destinationDirectory;
+    private ConversionWarnReceiver conversionWarnReceiver = new LoggingWarnReceiver();
     private boolean createDestinationDirectory;
     private boolean executed;
     private Set<File> directoriesKnownToExist = new HashSet<>();
@@ -70,7 +71,15 @@ public abstract class Converter {
         this.createDestinationDirectory = createDestinationDirectory;
     }
 
-    public final void execute()  throws ConverterException {
+    public ConversionWarnReceiver getConversionWarnReceiver() {
+        return conversionWarnReceiver;
+    }
+
+    public void setConversionWarnReceiver(ConversionWarnReceiver conversionWarnReceiver) {
+        this.conversionWarnReceiver = conversionWarnReceiver;
+    }
+
+    public final void execute() throws ConverterException {
         if (executed) {
             throw new IllegalStateException("This converted was already invoked once.");
         }
@@ -132,11 +141,13 @@ public abstract class Converter {
             LOG.debug("Converting file: {}", src);
             FileConversionContext fileTransCtx = null;
             try {
-                fileTransCtx = new FileConversionContext(srcStream, src, dstDir);
+                conversionWarnReceiver.setSourceFile(src);
+                fileTransCtx = new FileConversionContext(srcStream, src, dstDir, conversionWarnReceiver);
                 convertFile(fileTransCtx);
             } catch (IOException e) {
                 throw new ConverterException("I/O exception while converting " + _StringUtil.jQuote(src) + ".", e);
             } finally {
+                conversionWarnReceiver.setSourceFile(null);
                 try {
                     if (fileTransCtx != null && fileTransCtx.outputStream != null) {
                         fileTransCtx.outputStream.close();
@@ -198,14 +209,16 @@ public abstract class Converter {
         private final InputStream sourceStream;
         private final File sourceFile;
         private final File dstDir;
+        private final ConversionWarnReceiver conversionWarnReceiver;
         private String destinationFileName;
         private OutputStream outputStream;
 
         public FileConversionContext(
-                InputStream sourceStream, File sourceFile, File dstDir) {
+                InputStream sourceStream, File sourceFile, File dstDir, ConversionWarnReceiver conversionWarnReceiver) {
             this.sourceStream = sourceStream;
             this.sourceFile = sourceFile;
             this.dstDir = dstDir;
+            this.conversionWarnReceiver = conversionWarnReceiver;
         }
 
         /**
@@ -229,7 +242,7 @@ public abstract class Converter {
 
         /**
          * Write the content of the destination file with this.  You need not close this stream in
-s         * {@link Converter#convertFile(FileConversionContext)}; the {@link Converter} will do that.
+         * s         * {@link Converter#convertFile(FileConversionContext)}; the {@link Converter} will do that.
          */
         public OutputStream getDestinationStream() throws ConverterException {
             if (outputStream == null) {
@@ -255,7 +268,9 @@ s         * {@link Converter#convertFile(FileConversionContext)}; the {@link Con
 
         /**
          * Sets the name of the file where the output will be written.
-         * @param destinationFileName Can't contain directory name, only the file name.
+         *
+         * @param destinationFileName
+         *         Can't contain directory name, only the file name.
          */
         public void setDestinationFileName(String destinationFileName) {
             if (outputStream != null) {
@@ -268,6 +283,11 @@ s         * {@link Converter#convertFile(FileConversionContext)}; the {@link Con
             }
             this.destinationFileName = destinationFileName;
         }
+
+        public ConversionWarnReceiver getConversionWarnReceiver() {
+            return conversionWarnReceiver;
+        }
+
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/9b31510b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
index a70c08e..db2a272 100644
--- a/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
+++ b/freemarker-converter/src/main/java/org/apache/freemarker/converter/FM2ToFM3Converter.java
@@ -93,8 +93,9 @@ public class FM2ToFM3Converter extends Converter {
     @Override
     protected void convertFile(FileConversionContext fileTransCtx) throws ConverterException, IOException {
         String src = IOUtils.toString(fileTransCtx.getSourceStream(), StandardCharsets.UTF_8);
-        FM2ASTToFM3SourceConverter.Result result = FM2ASTToFM3SourceConverter.convert(fileTransCtx.getSourceFile()
-                .getName(), src, fm2Cfg);
+        FM2ASTToFM3SourceConverter.Result result = FM2ASTToFM3SourceConverter.convert(
+                fileTransCtx.getSourceFile().getName(), src, fm2Cfg, fileTransCtx.getConversionWarnReceiver()
+        );
         fileTransCtx.setDestinationFileName(getDestinationFileName(result.getFM2Template()));
         fileTransCtx.getDestinationStream().write(
                 result.getFM3Content().getBytes(getTemplateEncoding(result.getFM2Template())));

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/9b31510b/freemarker-converter/src/main/java/org/apache/freemarker/converter/LoggingWarnReceiver.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/org/apache/freemarker/converter/LoggingWarnReceiver.java b/freemarker-converter/src/main/java/org/apache/freemarker/converter/LoggingWarnReceiver.java
new file mode 100644
index 0000000..b2a2aa9
--- /dev/null
+++ b/freemarker-converter/src/main/java/org/apache/freemarker/converter/LoggingWarnReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import java.io.File;
+
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggingWarnReceiver implements ConversionWarnReceiver {
+
+    private File sourceFile;
+
+    private static final Logger LOG = LoggerFactory.getLogger(LoggingWarnReceiver.class);
+
+    @Override
+    public void setSourceFile(File sourceFile) {
+        this.sourceFile = sourceFile;
+    }
+
+    @Override
+    public void warn(int row, int col, String message) {
+        _NullArgumentException.check("message", message);
+        LOG.warn("{}:{}:{}: {}", sourceFile, row, col, message);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/9b31510b/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 7e9527f..362182c 100644
--- a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
+++ b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
@@ -235,6 +235,24 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
 
         assertConvertedSame("<#import '/lib/foo.ftl' as foo >");
         assertConvertedSame("<#import <#--1--> '/lib/foo.ftl' <#--2--> as <#--3--> foo <#--4--> >");
+
+        assertConvertedSame("<#include 'foo.ftl'>");
+        assertConverted("<#include 'foo.ftl' ignoreMissing=true>", "<#include 'foo.ftl' ignore_missing=true>");
+        assertConverted("<#include 'foo.ftl' ignoreMissing=true>",
+                "<#include 'foo.ftl' ignore_missing=true encoding='utf-8' parse=false>");
+        assertConverted("<#include 'foo.ftl' ignoreMissing=true>",
+                "<#include 'foo.ftl' encoding='utf-8' ignore_missing=true parse=false>");
+        assertConverted("<#include 'foo.ftl' ignoreMissing=true>",
+                "<#include 'foo.ftl' encoding='utf-8' parse=false ignore_missing=true>");
+        assertConvertedSame("<#include <#--1--> 'foo.ftl' <#--2--> >");
+        assertConvertedSame("<#include <#--1--> 'foo.ftl' <#--2--> ignoreMissing=true <#--3--> >");
+        assertConverted("<#include <#--1--> 'foo.ftl' <#--2-->>",
+                "<#include <#--1--> 'foo.ftl' <#--2--> parse=true <#--3--> >");
+        assertConverted("<#include <#--1--> 'foo.ftl' <#--2--> ignoreMissing=true <#--3-->>",
+                "<#include <#--1--> 'foo.ftl' <#--2--> ignoreMissing=true <#--3--> parse=true <#--4--> >");
+        assertConverted("<#include <#--1--> 'foo.ftl' <#--2--> ignoreMissing=true <#--4-->>",
+                "<#include <#--1--> 'foo.ftl' <#--2--> encoding='UTF-8' <#--3--> ignoreMissing=true <#--4--> "
+                        + "parse=true <#--5--> >");
     }
 
     @Test