When a macro uses .args, and has catch-all parameter, allow positional macro arguments when the actual catch-all length is 0. Also, added some tests for 0 argument calls.
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index b64e9e4..17c63f6 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -37,6 +37,7 @@
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template.utility.Constants;
 
 /**
  * An element representing a macro or function declaration.
@@ -323,7 +324,8 @@
             
             if (argsSpecVarDraft != null) {
                 final String catchAllParamName = getMacro().catchAllParamName;
-                final TemplateModel catchAllArgValue = catchAllParamName != null ? localVars.get(catchAllParamName) : null;
+                final TemplateModel catchAllArgValue = catchAllParamName != null
+                        ? localVars.get(catchAllParamName) : null;
 
                 if (getMacro().isFunction()) {
                     int lengthWithCatchAlls = argsSpecVarDraft.length;
@@ -350,10 +352,15 @@
                     TemplateHashModelEx2 catchAllHash;
                     if (catchAllParamName != null) {
                         if (catchAllArgValue instanceof TemplateSequenceModel) {
-                            throw new _MiscTemplateException("The macro can only by called with named arguments, " +
-                                    "because it uses both .", BuiltinVariable.ARGS, " and catch-all parameter.");
+                            if (((TemplateSequenceModel) catchAllArgValue).size() != 0) {
+                                throw new _MiscTemplateException("The macro can only by called with named arguments, " +
+                                        "because it uses both .", BuiltinVariable.ARGS, " and a non-empty catch-all " +
+                                        "parameter.");
+                            }
+                            catchAllHash = Constants.EMPTY_HASH_EX2;
+                        } else {
+                            catchAllHash = (TemplateHashModelEx2) catchAllArgValue;
                         }
-                        catchAllHash = (TemplateHashModelEx2) catchAllArgValue;
                         lengthWithCatchAlls += catchAllHash.size();
                     } else {
                         catchAllHash = null;
diff --git a/src/main/java/freemarker/template/utility/Constants.java b/src/main/java/freemarker/template/utility/Constants.java
index 2162ef6..8392c01 100644
--- a/src/main/java/freemarker/template/utility/Constants.java
+++ b/src/main/java/freemarker/template/utility/Constants.java
@@ -95,9 +95,14 @@
         }
         
     }
-    
-    public static final TemplateHashModelEx EMPTY_HASH = new EmptyHashModel();
-    
+
+    /**
+     * @since 2.3.30
+     */
+    public static final TemplateHashModelEx2 EMPTY_HASH_EX2 = new EmptyHashModel();
+
+    public static final TemplateHashModelEx EMPTY_HASH = EMPTY_HASH_EX2;
+
     /**
      * An empty hash. Since 2.3.27, it implements {@link TemplateHashModelEx2}, before that it was only
      * {@link TemplateHashModelEx}.
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 3b1432c..6c03186 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -24617,7 +24617,8 @@
             </listitem>
 
             <listitem>
-              <para>If a macro has a catch-all argument, and the macro uses
+              <para>If a macro has a catch-all parameter, and the actual
+              catch-all argument is not empty, and the macro uses
               <literal>.args</literal> somewhere, it can only be called with
               named arguments (like <literal>&lt;@m a=1 b=2 /&gt;</literal>),
               and not with positional arguments (like <literal>&lt;@m 1 2
diff --git a/src/test/java/freemarker/core/ArgsSpecialVariableTest.java b/src/test/java/freemarker/core/ArgsSpecialVariableTest.java
index 1064744..bc94cef 100644
--- a/src/test/java/freemarker/core/ArgsSpecialVariableTest.java
+++ b/src/test/java/freemarker/core/ArgsSpecialVariableTest.java
@@ -41,6 +41,12 @@
     }
 
     @Test
+    public void macroZeroArgsTest() throws IOException, TemplateException {
+        assertOutput("<#macro m>${.args?size}</#macro><@m />", "0");
+        assertOutput("<#macro m others...>${.args?size}</#macro><@m />", "0");
+    }
+
+    @Test
     public void macroWithDefaultsTest() throws IOException, TemplateException {
         String macroDef = "<#macro m a b c=3><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>";
         String expectedOutput = "" +
@@ -80,8 +86,8 @@
 
     @Test
     public void macroWithCatchAllTest() throws IOException, TemplateException {
-        assertOutput("" +
-                        "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>" +
+        String macroDef = "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>";
+        assertOutput(macroDef +
                         "<@m a=11 b=22 c=33 d=44 />; " +
                         "<@m a=11 b=22 />; " +
                         "<@m a=11 />; " +
@@ -91,9 +97,9 @@
                         "a=11, b=2; " +
                         "a=11, b=2, c=33");
 
-        assertErrorContains("" +
-                "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>" +
-                "<@m 1, 2 />",
+        assertOutput(macroDef + "<@m 1, 2 />",
+                "a=1, b=2");
+        assertErrorContains(macroDef + "<@m 1, 2, 3 />",
                 ".args", "catch-all");
     }
 
@@ -106,6 +112,13 @@
                 expectedOutput);
     }
 
+
+    @Test
+    public void functionZeroArgsTest() throws IOException, TemplateException {
+        assertOutput("<#function f><#return .args?size></#function>${f()}", "0");
+        assertOutput("<#function f others...><#return .args?size></#function>${f()}", "0");
+    }
+    
     @Test
     public void functionWithDefaultsTest() throws IOException, TemplateException {
         String functionDef = "<#function f a b c=3><#return .args?join(', ')></#function>";
@@ -150,7 +163,7 @@
                         "11, 2; " +
                         "11, 2, 33");
     }
-
+    
     @Test
     public void usedInWrongContextTest() throws IOException, TemplateException {
         assertErrorContains("${.args}", "args", "macro", "function");