Repository: incubator-freemarker Updated Branches: refs/heads/3 89d436965 -> 1b6f894ee
FREEMARKER-64: Function (and thus also method) call syntax now supports passing parameter by name. Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/b59a03a8 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/b59a03a8 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/b59a03a8 Branch: refs/heads/3 Commit: b59a03a8add0f47427ddd9594852f7bd86a8a7e9 Parents: 8d5263f Author: ddekany <ddek...@apache.org> Authored: Tue Aug 8 13:46:50 2017 +0200 Committer: ddekany <ddek...@apache.org> Committed: Tue Aug 8 13:46:50 2017 +0200 ---------------------------------------------------------------------- .../core/TemplateCallableModelTest.java | 36 +++- .../core/userpkg/AllFeaturesFunction.java | 29 ++- .../core/userpkg/NamedVarargsOnlyFunction.java | 61 ++++++ .../userpkg/PositionalVarargsOnlyFunction.java | 19 +- .../core/userpkg/TestTemplateCallableModel.java | 58 +++--- .../core/userpkg/TwoNamedParamsFunction.java | 71 +++++++ .../userpkg/TwoPositionalParamsFunction.java | 21 +- .../templates/string-builtins3.ftl | 16 +- .../freemarker/core/ASTDynamicTopLevelCall.java | 154 +------------- .../freemarker/core/ASTExpFunctionCall.java | 201 +++++++++++-------- .../apache/freemarker/core/_CallableUtils.java | 180 ++++++++++++++++- .../core/model/ArgumentArrayLayout.java | 14 ++ freemarker-core/src/main/javacc/FTL.jj | 118 +++++++++-- 13 files changed, 650 insertions(+), 328 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java index 3459aea..d6c9e66 100644 --- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java @@ -24,9 +24,11 @@ import java.io.IOException; import org.apache.freemarker.core.userpkg.AllFeaturesDirective; import org.apache.freemarker.core.userpkg.AllFeaturesFunction; import org.apache.freemarker.core.userpkg.NamedVarargsOnlyDirective; +import org.apache.freemarker.core.userpkg.NamedVarargsOnlyFunction; import org.apache.freemarker.core.userpkg.PositionalVarargsOnlyDirective; import org.apache.freemarker.core.userpkg.PositionalVarargsOnlyFunction; import org.apache.freemarker.core.userpkg.TwoNamedParamsDirective; +import org.apache.freemarker.core.userpkg.TwoNamedParamsFunction; import org.apache.freemarker.core.userpkg.TwoNestedContentParamsDirective; import org.apache.freemarker.core.userpkg.TwoPositionalParamsDirective; import org.apache.freemarker.core.userpkg.TwoPositionalParamsFunction; @@ -48,7 +50,9 @@ public class TemplateCallableModelTest extends TemplateTest { addToDataModel("fa", AllFeaturesFunction.INSTANCE); addToDataModel("fp", TwoPositionalParamsFunction.INSTANCE); + addToDataModel("fn", TwoNamedParamsFunction.INSTANCE); addToDataModel("fpvo", PositionalVarargsOnlyFunction.INSTANCE); + addToDataModel("fnvo", NamedVarargsOnlyFunction.INSTANCE); } @Test @@ -121,6 +125,13 @@ public class TemplateCallableModelTest extends TemplateTest { assertOutput("${fp(1, 2)}", "fp(p1=1, p2=2)"); + assertOutput("${fn()}", + "fn(n1=null, n2=null)"); + assertOutput("${fn(n1=11)}", + "fn(n1=11, n2=null)"); + assertOutput("${fn(n1=11, n2=22)}", + "fn(n1=11, n2=22)"); + assertOutput("${fpvo()}", "fpvo(pVarargs=[])"); assertOutput("${fpvo(1)}", @@ -132,10 +143,27 @@ public class TemplateCallableModelTest extends TemplateTest { "fa(p1=null, p2=null, pVarargs=[], n1=null, n2=null, nVarargs={})"); assertOutput("${fa(1, 2)}", "fa(p1=1, p2=2, pVarargs=[], n1=null, n2=null, nVarargs={})"); - assertOutput("${fa(1, 2, 3)}", - "fa(p1=1, p2=2, pVarargs=[3], n1=null, n2=null, nVarargs={})"); - assertOutput("${fa(1, 2, 3, 4)}", - "fa(p1=1, p2=2, pVarargs=[3, 4], n1=null, n2=null, nVarargs={})"); + assertOutput("${fa(n1=11, n2=22)}", + "fa(p1=null, p2=null, pVarargs=[], n1=11, n2=22, nVarargs={})"); + + assertOutput("${fa(1, 2, n1=11, n2=22)}", + "fa(p1=1, p2=2, pVarargs=[], n1=11, n2=22, nVarargs={})"); + assertOutput("${fa(1, n1=11)}", + "fa(p1=1, p2=null, pVarargs=[], n1=11, n2=null, nVarargs={})"); + assertOutput("${fa(1, 2, 3, n1=11, n2=22, n3=33)}", + "fa(p1=1, p2=2, pVarargs=[3], n1=11, n2=22, nVarargs={\"n3\": 33})"); + assertOutput("${fa(1, n1=11, n3=33)}", + "fa(p1=1, p2=null, pVarargs=[], n1=11, n2=null, nVarargs={\"n3\": 33})"); + assertOutput("${fa(1, n1=11, a=1, b=2, c=3, d=4, e=5, f=6, g=7)}", + "fa(p1=1, p2=null, pVarargs=[], n1=11, n2=null, nVarargs={" + + "\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4, \"e\": 5, \"f\": 6, \"g\": 7})"); + + assertOutput("${fnvo()}", + "fnvo(nVarargs={})"); + assertOutput("${fnvo(n1=11)}", + "fnvo(nVarargs={\"n1\": 11})"); + assertOutput("${fnvo(n1=11, n2=22)}", + "fnvo(nVarargs={\"n1\": 11, \"n2\": 22})"); } @Test http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesFunction.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesFunction.java index f468be7..66817bb 100644 --- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesFunction.java +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesFunction.java @@ -21,9 +21,6 @@ package org.apache.freemarker.core.userpkg; import static org.apache.freemarker.core._CallableUtils.*; -import java.io.IOException; -import java.io.StringWriter; - import org.apache.freemarker.core.CallPlace; import org.apache.freemarker.core.Environment; import org.apache.freemarker.core.TemplateException; @@ -88,20 +85,18 @@ public class AllFeaturesFunction extends TestTemplateCallableModel implements Te private TemplateModel execute(Number p1, Number p2, TemplateSequenceModel pOthers, Number n1, Number n2, TemplateHashModelEx2 nOthers) throws TemplateException { - StringWriter out = new StringWriter(); - try { - out.write("fa("); - printParam("p1", p1, out, true); - printParam("p2", p2, out); - printParam("pVarargs", pOthers, out); - printParam(N1_ARG_NAME, n1, out); - printParam(N2_ARG_NAME, n2, out); - printParam("nVarargs", nOthers, out); - out.write(")"); - } catch (IOException e) { - throw new IllegalStateException(e); - } - return new SimpleScalar(out.toString()); + StringBuilder sb = new StringBuilder(); + + sb.append("fa("); + printParam("p1", p1, sb, true); + printParam("p2", p2, sb); + printParam("pVarargs", pOthers, sb); + printParam(N1_ARG_NAME, n1, sb); + printParam(N2_ARG_NAME, n2, sb); + printParam("nVarargs", nOthers, sb); + sb.append(")"); + + return new SimpleScalar(sb.toString()); } @Override http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/NamedVarargsOnlyFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/NamedVarargsOnlyFunction.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/NamedVarargsOnlyFunction.java new file mode 100644 index 0000000..8413647 --- /dev/null +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/NamedVarargsOnlyFunction.java @@ -0,0 +1,61 @@ +/* + * 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.core.userpkg; + +import org.apache.freemarker.core.CallPlace; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.TemplateFunctionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; + +public class NamedVarargsOnlyFunction extends TestTemplateCallableModel implements TemplateFunctionModel { + + public static final NamedVarargsOnlyFunction INSTANCE = new NamedVarargsOnlyFunction(); + + private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create( + 0, false, + null, true); + + private static final int NAMED_VARARGS_ARG_IDX = ARGS_LAYOUT.getNamedVarargsArgumentIndex(); + + private NamedVarargsOnlyFunction() { + // + } + + @Override + public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) + throws TemplateException { + StringBuilder sb = new StringBuilder(); + + sb.append("fnvo("); + printParam("nVarargs", args[NAMED_VARARGS_ARG_IDX], sb, true); + sb.append(")"); + + return new SimpleScalar(sb.toString()); + } + + @Override + public ArgumentArrayLayout getFunctionArgumentArrayLayout() { + return ARGS_LAYOUT; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PositionalVarargsOnlyFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PositionalVarargsOnlyFunction.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PositionalVarargsOnlyFunction.java index 1c0c82c..7c8040d 100644 --- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PositionalVarargsOnlyFunction.java +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/PositionalVarargsOnlyFunction.java @@ -19,9 +19,6 @@ package org.apache.freemarker.core.userpkg; -import java.io.IOException; -import java.io.StringWriter; - import org.apache.freemarker.core.CallPlace; import org.apache.freemarker.core.Environment; import org.apache.freemarker.core.TemplateException; @@ -44,15 +41,13 @@ public class PositionalVarargsOnlyFunction extends TestTemplateCallableModel imp @Override public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws TemplateException { - try { - StringWriter out = new StringWriter(); - out.write("fpvo("); - printParam("pVarargs", args[ARGS_LAYOUT.getPositionalVarargsArgumentIndex()], out, true); - out.write(")"); - return new SimpleScalar(out.toString()); - } catch (IOException e) { - throw new IllegalStateException(e); - } + StringBuilder sb = new StringBuilder(); + + sb.append("fpvo("); + printParam("pVarargs", args[ARGS_LAYOUT.getPositionalVarargsArgumentIndex()], sb, true); + sb.append(")"); + + return new SimpleScalar(sb.toString()); } @Override http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateCallableModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateCallableModel.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateCallableModel.java index f0a15d7..a1b263c 100644 --- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateCallableModel.java +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateCallableModel.java @@ -33,62 +33,72 @@ import org.apache.freemarker.core.util._StringUtil; public abstract class TestTemplateCallableModel implements TemplateCallableModel { + protected void printParam(String name, Object value, StringBuilder sb) throws TemplateModelException { + printParam(name, value, sb, false); + } + + protected void printParam(String name, Object value, StringBuilder sb, boolean first) + throws TemplateModelException { + if (!first) { + sb.append(", "); + } + sb.append(name); + sb.append("="); + printValue(value, sb); + } + protected void printParam(String name, Object value, Writer out) throws IOException, TemplateModelException { printParam(name, value, out, false); } protected void printParam(String name, Object value, Writer out, boolean first) throws IOException, TemplateModelException { - if (!first) { - out.write(", "); - } - out.write(name); - out.write("="); - printValue(value, out); + StringBuilder sb = new StringBuilder(); + printParam(name, value, sb, first); + out.write(sb.toString()); } - private void printValue(Object value, Writer out) throws IOException, TemplateModelException { + private void printValue(Object value, StringBuilder sb) throws TemplateModelException { if (value == null) { - out.write("null"); + sb.append("null"); } else if (value instanceof TemplateNumberModel) { - out.write(((TemplateNumberModel) value).getAsNumber().toString()); + sb.append(((TemplateNumberModel) value).getAsNumber().toString()); } else if (value instanceof TemplateScalarModel) { - out.write(FTLUtil.toStringLiteral(((TemplateScalarModel) value).getAsString())); + sb.append(FTLUtil.toStringLiteral(((TemplateScalarModel) value).getAsString())); } else if (value instanceof TemplateSequenceModel) { int len = ((TemplateSequenceModel) value).size(); - out.write('['); + sb.append('['); for (int i = 0; i < len; i++) { if (i != 0) { - out.write(", "); + sb.append(", "); } - printValue(((TemplateSequenceModel) value).get(i), out); + printValue(((TemplateSequenceModel) value).get(i), sb); } - out.write(']'); + sb.append(']'); } else if (value instanceof TemplateHashModelEx2) { TemplateHashModelEx2.KeyValuePairIterator it = ((TemplateHashModelEx2) value).keyValuePairIterator(); - out.write('{'); + sb.append('{'); while (it.hasNext()) { TemplateHashModelEx2.KeyValuePair kvp = it.next(); - printValue(kvp.getKey(), out); - out.write(": "); - printValue(kvp.getValue(), out); + printValue(kvp.getKey(), sb); + sb.append(": "); + printValue(kvp.getValue(), sb); if (it.hasNext()) { - out.write(", "); + sb.append(", "); } } - out.write('}'); + sb.append('}'); } else if (value instanceof String) { - out.write(_StringUtil.jQuote(value)); + sb.append(_StringUtil.jQuote(value)); } else if (value instanceof Number) { - out.write(value.toString()); + sb.append(value.toString()); } else if (value instanceof Boolean) { - out.write(value.toString()); + sb.append(value.toString()); } else { throw new IllegalArgumentException("Unsupported value class: " + value.getClass().getName()); } } - } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsFunction.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsFunction.java new file mode 100644 index 0000000..b866034 --- /dev/null +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsFunction.java @@ -0,0 +1,71 @@ +/* + * 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.core.userpkg; + +import org.apache.freemarker.core.CallPlace; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.TemplateFunctionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.util.StringToIndexMap; + +public class TwoNamedParamsFunction extends TestTemplateCallableModel implements TemplateFunctionModel { + + public static final TwoNamedParamsFunction INSTANCE = new TwoNamedParamsFunction(); + + private static final int N1_ARG_IDX = 0; + private static final int N2_ARG_IDX = 1; + + private static final String N1_ARG_NAME = "n1"; + private static final String N2_ARG_NAME = "n2"; + + private static final StringToIndexMap ARG_NAME_TO_IDX = StringToIndexMap.of( + N1_ARG_NAME, N1_ARG_IDX, + N2_ARG_NAME, N2_ARG_IDX); + + private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create( + 0, false, + ARG_NAME_TO_IDX, false); + + private TwoNamedParamsFunction() { + // + } + + @Override + public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) + throws TemplateException { + StringBuilder sb = new StringBuilder(); + + sb.append("fn("); + printParam(N1_ARG_NAME, args[N1_ARG_IDX], sb, true); + printParam(N2_ARG_NAME, args[N2_ARG_IDX], sb); + sb.append(")"); + + return new SimpleScalar(sb.toString()); + } + + @Override + public ArgumentArrayLayout getFunctionArgumentArrayLayout() { + return ARGS_LAYOUT; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsFunction.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsFunction.java index e406c99..cfce364 100644 --- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsFunction.java +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsFunction.java @@ -19,9 +19,6 @@ package org.apache.freemarker.core.userpkg; -import java.io.IOException; -import java.io.StringWriter; - import org.apache.freemarker.core.CallPlace; import org.apache.freemarker.core.Environment; import org.apache.freemarker.core.TemplateException; @@ -44,16 +41,14 @@ public class TwoPositionalParamsFunction extends TestTemplateCallableModel imple @Override public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws TemplateException { - try { - StringWriter out = new StringWriter(); - out.write("fp("); - printParam("p1", args[0], out, true); - printParam("p2", args[1], out); - out.write(")"); - return new SimpleScalar(out.toString()); - } catch (IOException e) { - throw new IllegalStateException(e); - } + StringBuilder sb = new StringBuilder(); + + sb.append("fp("); + printParam("p1", args[0], sb, true); + printParam("p2", args[1], sb); + sb.append(")"); + + return new SimpleScalar(sb.toString()); } @Override http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/string-builtins3.ftl ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/string-builtins3.ftl b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/string-builtins3.ftl index ece74f9..0a387ce 100644 --- a/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/string-builtins3.ftl +++ b/freemarker-core-test/src/test/resources/org/apache/freemarker/core/templatesuite/templates/string-builtins3.ftl @@ -35,7 +35,7 @@ <@assertFails message='"m" flag'> ${'x'?keepBefore('x', 'm')} </@assertFails> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?keepBefore('x', 'i', 'x')} </@assertFails> <@assertFails message='argument'> @@ -61,7 +61,7 @@ <@assertFails message='"m" flag'> ${'x'?keepBeforeLast('x', 'm')} </@assertFails> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?keepBeforeLast('x', 'i', 'x')} </@assertFails> <@assertFails message='argument'> @@ -87,7 +87,7 @@ <@assertFails message='"m" flag'> ${'x'?keepAfter('x', 'm')} </@assertFails> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?keepAfter('x', 'i', 'x')} </@assertFails> <@assertFails message='argument'> @@ -115,7 +115,7 @@ <@assertFails message='"m" flag'> ${'x'?keepAfterLast('x', 'm')} </@assertFails> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?keepAfterLast('x', 'i', 'x')} </@assertFails> <@assertFails message='argument'> @@ -129,7 +129,7 @@ <@assertEquals expected='o' actual='foo'?removeBeginning('fo') /> <@assertEquals expected='' actual='foo'?removeBeginning('foo') /> <@assertEquals expected='foo' actual='foo'?removeBeginning('') /> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?removeBeginning('x', 'x')} </@assertFails> <@assertFails message='argument'> @@ -143,7 +143,7 @@ <@assertEquals expected='b' actual='bar'?removeEnding('ar') /> <@assertEquals expected='' actual='bar'?removeEnding('bar') /> <@assertEquals expected='bar' actual='bar'?removeEnding('') /> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?removeEnding('x', 'x')} </@assertFails> <@assertFails message='argument'> @@ -171,7 +171,7 @@ <@assertEquals expected='https://example.com' actual="https://example.com"?ensureStartsWith("[a-z]+://", "http://") /> <@assertEquals expected='http://HTTP://example.com' actual="HTTP://example.com"?ensureStartsWith("[a-z]+://", "http://") /> <@assertEquals expected='HTTP://example.com' actual="HTTP://example.com"?ensureStartsWith("[a-z]+://", "http://", "ir") /> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?ensureStartsWith('x', 'x', 'x', 'x')} </@assertFails> <@assertFails message='argument'> @@ -185,7 +185,7 @@ <@assertEquals expected='foo' actual='foo'?ensureEndsWith('') /> <@assertEquals expected='x' actual=''?ensureEndsWith('x') /> <@assertEquals expected='' actual=''?ensureEndsWith('') /> -<@assertFails message='Too many arguments'> +<@assertFails message='can only have'> ${'x'?ensureEndsWith('x', 'x')} </@assertFails> <@assertFails message='argument'> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDynamicTopLevelCall.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDynamicTopLevelCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDynamicTopLevelCall.java index f579f6f..7bf4737 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDynamicTopLevelCall.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDynamicTopLevelCall.java @@ -21,22 +21,17 @@ package org.apache.freemarker.core; import java.io.IOException; import java.io.Writer; -import java.util.Collection; -import java.util.List; import org.apache.freemarker.core.ThreadInterruptionSupportTemplatePostProcessor.ASTThreadInterruptionCheck; +import org.apache.freemarker.core._CallableUtils.NamedArgument; import org.apache.freemarker.core.model.ArgumentArrayLayout; -import org.apache.freemarker.core.model.Constants; import org.apache.freemarker.core.model.TemplateCallableModel; import org.apache.freemarker.core.model.TemplateDirectiveModel; import org.apache.freemarker.core.model.TemplateFunctionModel; import org.apache.freemarker.core.model.TemplateModel; -import org.apache.freemarker.core.model.TemplateSequenceModel; import org.apache.freemarker.core.util.BugException; import org.apache.freemarker.core.util.CommonSupplier; -import org.apache.freemarker.core.util.FTLUtil; import org.apache.freemarker.core.util.StringToIndexMap; -import org.apache.freemarker.core.util._CollectionUtil; import org.apache.freemarker.core.util._StringUtil; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -54,16 +49,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; */ class ASTDynamicTopLevelCall extends ASTDirective implements CallPlace { - static final class NamedArgument { - private final String name; - private final ASTExpression value; - - public NamedArgument(String name, ASTExpression value) { - this.name = name; - this.value = value; - } - } - private final ASTExpression callableValueExp; private final ASTExpression[] positionalArgs; private final NamedArgument[] namedArgs; @@ -132,9 +117,8 @@ class ASTDynamicTopLevelCall extends ASTDirective implements CallPlace { throw new _MiscTemplateException(env, "Nested content is not supported by this directive."); } - TemplateModel[] execArgs = argsLayout != null - ? getExecuteArgsBasedOnLayout(argsLayout, callableValue, env) - : getExecuteArgsWithoutLayout(callableValue, env); + TemplateModel[] execArgs = _CallableUtils.getExecuteArgs( + positionalArgs, namedArgs, argsLayout, callableValue, env); if (directive != null) { directive.execute(execArgs, this, env.getOut(), env); @@ -150,129 +134,6 @@ class ASTDynamicTopLevelCall extends ASTDirective implements CallPlace { return null; } - private TemplateModel[] getExecuteArgsWithoutLayout(TemplateCallableModel callableValue, Environment env) - throws TemplateException { - if (namedArgs != null) { - throw new _MiscTemplateException(env, getNamedArgumentsNotSupportedMessage(callableValue, namedArgs[0])); - } - TemplateModel[] execArgs = new TemplateModel[positionalArgs.length]; - for (int i = 0; i < positionalArgs.length; i++) { - ASTExpression positionalArg = positionalArgs[i]; - execArgs[i] = positionalArg.eval(env); - } - return execArgs; - } - - private TemplateModel[] getExecuteArgsBasedOnLayout(ArgumentArrayLayout argsLayout, TemplateCallableModel callableValue, - Environment env) throws TemplateException { - int predefPosArgCnt = argsLayout.getPredefinedPositionalArgumentCount(); - int posVarargsArgIdx = argsLayout.getPositionalVarargsArgumentIndex(); - - TemplateModel[] execArgs = new TemplateModel[argsLayout.getTotalLength()]; - - // Fill predefined positional args: - if (positionalArgs != null) { - int actualPredefPosArgCnt = Math.min(positionalArgs.length, predefPosArgCnt); - for (int argIdx = 0; argIdx < actualPredefPosArgCnt; argIdx++) { - execArgs[argIdx] = positionalArgs[argIdx].eval(env); - } - } - - if (posVarargsArgIdx != -1) { - int posVarargsLength = positionalArgs != null ? positionalArgs.length - predefPosArgCnt : 0; - TemplateSequenceModel varargsSeq; - if (posVarargsLength <= 0) { - varargsSeq = Constants.EMPTY_SEQUENCE; - } else { - NativeSequence nativeSeq = new NativeSequence(posVarargsLength); - varargsSeq = nativeSeq; - for (int posVarargIdx = 0; posVarargIdx < posVarargsLength; posVarargIdx++) { - nativeSeq.add(positionalArgs[predefPosArgCnt + posVarargIdx].eval(env)); - } - } - execArgs[posVarargsArgIdx] = varargsSeq; - } else if (positionalArgs != null && positionalArgs.length > predefPosArgCnt) { - checkSupportsAnyParameters(callableValue, argsLayout, env); - List<String> validPredefNames = argsLayout.getPredefinedNamedArgumentsMap().getKeys(); - _ErrorDescriptionBuilder errorDesc = new _ErrorDescriptionBuilder( - "The target ", FTLUtil.getCallableTypeName(callableValue), " ", - (predefPosArgCnt != 0 - ? new Object[]{ "can only have ", predefPosArgCnt } - : "can't have" - ), - " arguments passed by position, but the invocation has ", - positionalArgs.length, " such arguments. Try to pass arguments by name (as in ", - "<@example x=1 y=2 />", ").", - (!validPredefNames.isEmpty() - ? new Object[] { " The supported parameter names are:\n", - new _DelayedJQuotedListing(validPredefNames)} - : _CollectionUtil.EMPTY_OBJECT_ARRAY) - ); - if (callableValue instanceof Environment.TemplateLanguageDirective) { - errorDesc.tip("You can pass a parameter by position (i.e., without specifying its name, as you" - + " have tried now) when the macro has defined that parameter to be a positional parameter. " - + "See in the documentation how, and when that's a good practice."); - } - throw new _MiscTemplateException(env, - errorDesc - ); - } - - int namedVarargsArgumentIndex = argsLayout.getNamedVarargsArgumentIndex(); - NativeHashEx2 namedVarargsHash = null; - if (namedArgs != null) { - StringToIndexMap predefNamedArgsMap = argsLayout.getPredefinedNamedArgumentsMap(); - for (NamedArgument namedArg : namedArgs) { - int argIdx = predefNamedArgsMap.get(namedArg.name); - if (argIdx != -1) { - execArgs[argIdx] = namedArg.value.eval(env); - } else { - if (namedVarargsHash == null) { - if (namedVarargsArgumentIndex == -1) { - checkSupportsAnyParameters(callableValue, argsLayout, env); - Collection<String> validNames = predefNamedArgsMap.getKeys(); - throw new _MiscTemplateException(env, - validNames == null || validNames.isEmpty() - ? getNamedArgumentsNotSupportedMessage(callableValue, namedArg) - : new Object[] { - "The called ", FTLUtil.getCallableTypeName(callableValue), - " has no parameter that's passed by name and is called ", - new _DelayedJQuote(namedArg.name), ". The supported parameter names are:\n", - new _DelayedJQuotedListing(validNames) - }); - } - - namedVarargsHash = new NativeHashEx2(); - } - namedVarargsHash.put(namedArg.name, namedArg.value.eval(env)); - } - } - } - if (namedVarargsArgumentIndex != -1) { - execArgs[namedVarargsArgumentIndex] = namedVarargsHash != null ? namedVarargsHash : Constants.EMPTY_HASH; - } - return execArgs; - } - - private Object[] getNamedArgumentsNotSupportedMessage(TemplateCallableModel callableValue, - NamedArgument namedArg) { - return new Object[] { - "The called ", FTLUtil.getCallableTypeName(callableValue), - " can't have arguments that are passed by name (like ", - new _DelayedJQuote(namedArg.name), "). Try to pass arguments by position " - + "(i.e, without name, as in ", "<@example 1, 2, 3 />" , ")." - }; - } - - private void checkSupportsAnyParameters( - TemplateCallableModel callableValue, ArgumentArrayLayout argsLayout, Environment env) - throws TemplateException { - if (argsLayout.getTotalLength() == 0) { - throw new _MiscTemplateException(env, - "The called ", FTLUtil.getCallableTypeName(callableValue), " doesn't support any parameters."); - } - } - @Override boolean isNestedBlockRepeater() { return true; @@ -292,20 +153,19 @@ class ASTDynamicTopLevelCall extends ASTDirective implements CallPlace { boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')'; if (positionalArgs != null) { for (int i = 0; i < positionalArgs.length; i++) { - ASTExpression argExp = (ASTExpression) positionalArgs[i]; if (i != 0) { sb.append(','); } sb.append(' '); - sb.append(argExp.getCanonicalForm()); + sb.append(positionalArgs[i].getCanonicalForm()); } } if (namedArgs != null) { for (NamedArgument namedArg : namedArgs) { sb.append(' '); - sb.append(_StringUtil.toFTLTopLevelIdentifierReference(namedArg.name)); + sb.append(_StringUtil.toFTLTopLevelIdentifierReference(namedArg.getName())); sb.append('='); - MessageUtil.appendExpressionAsUntearable(sb, namedArg.value); + sb.append(namedArg.getValue().getCanonicalForm()); } } if (nestedContentParamNames != null) { @@ -365,7 +225,7 @@ class ASTDynamicTopLevelCall extends ASTDirective implements CallPlace { final int namedArgsSize = namedArgs != null ? namedArgs.length : 0; if (idx - base < namedArgsSize * 2) { NamedArgument namedArg = namedArgs[(idx - base) / 2]; - return (idx - base) % 2 == 0 ? namedArg.name : namedArg.value; + return (idx - base) % 2 == 0 ? namedArg.getName() : namedArg.getValue(); } else { base += namedArgsSize * 2; final int bodyParameterNamesSize = nestedContentParamNames != null http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpFunctionCall.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpFunctionCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpFunctionCall.java index 40dc6e3..5101db7 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpFunctionCall.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpFunctionCall.java @@ -25,16 +25,12 @@ package org.apache.freemarker.core; import java.io.IOException; import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import org.apache.freemarker.core.model.ArgumentArrayLayout; -import org.apache.freemarker.core.model.Constants; +import org.apache.freemarker.core._CallableUtils.NamedArgument; import org.apache.freemarker.core.model.TemplateFunctionModel; import org.apache.freemarker.core.model.TemplateModel; -import org.apache.freemarker.core.model.TemplateSequenceModel; import org.apache.freemarker.core.util.CommonSupplier; -import org.apache.freemarker.core.util.FTLUtil; +import org.apache.freemarker.core.util._StringUtil; /** @@ -42,93 +38,73 @@ import org.apache.freemarker.core.util.FTLUtil; */ final class ASTExpFunctionCall extends ASTExpression implements CallPlace { - private final ASTExpression target; - private final ASTExpListLiteral arguments; + private final ASTExpression functionExp; + private final ASTExpression[] positionalArgs; + private final NamedArgument[] namedArgs; - ASTExpFunctionCall(ASTExpression target, ArrayList arguments) { - this(target, new ASTExpListLiteral(arguments)); - } + ASTExpFunctionCall( + ASTExpression functionExp, ASTExpression[] positionalArgs, NamedArgument[] namedArgs) { + this.functionExp = functionExp; + + if (positionalArgs != null && positionalArgs.length == 0 + || namedArgs != null && namedArgs.length == 0) { + throw new IllegalArgumentException("Use null instead of empty collections"); + } - private ASTExpFunctionCall(ASTExpression target, ASTExpListLiteral arguments) { - this.target = target; - this.arguments = arguments; + this.positionalArgs = positionalArgs; + this.namedArgs = namedArgs; } @Override TemplateModel _eval(Environment env) throws TemplateException { - TemplateModel targetModel = target.eval(env); - - if (!(targetModel instanceof TemplateFunctionModel)) { - throw new NonFunctionException(target, targetModel, env); - } - TemplateFunctionModel func = (TemplateFunctionModel) targetModel; - - ArgumentArrayLayout arrayLayout = func.getFunctionArgumentArrayLayout(); - - // TODO [FM3] This is just temporary, until we support named args. Then the logic in ASTDynamicTopLevelCall - // should be reused. - - TemplateModel[] args; - if (arrayLayout != null) { - int posVarargsLength; - int callArgCnt = arguments.size(); - int predefPosArgCnt = arrayLayout.getPredefinedPositionalArgumentCount(); - int posVarargsIdx = arrayLayout.getPositionalVarargsArgumentIndex(); - if (callArgCnt > predefPosArgCnt) { - if (posVarargsIdx == -1) { - throw new _MiscTemplateException(env, - "Too many arguments; the target ", FTLUtil.getCallableTypeName(func), - " only has ", predefPosArgCnt, " parameters."); - } + TemplateFunctionModel function; + { + TemplateModel functionUncasted = functionExp.eval(env); + if (!(functionUncasted instanceof TemplateFunctionModel)) { + throw new NonFunctionException(functionExp, functionUncasted, env); } + function = (TemplateFunctionModel) functionUncasted; + } - List<TemplateModel> callArgList = arguments.getModelList(env); - - args = new TemplateModel[arrayLayout.getTotalLength()]; - int callPredefArgCnt = Math.min(callArgCnt, predefPosArgCnt); - for (int argIdx = 0; argIdx < callPredefArgCnt; argIdx++) { - args[argIdx] = callArgList.get(argIdx); - } + return function.execute( + _CallableUtils.getExecuteArgs( + positionalArgs, namedArgs, function.getFunctionArgumentArrayLayout(), function, env), + this, + env); + } - if (posVarargsIdx != -1) { - TemplateSequenceModel varargsSeq; - posVarargsLength = callArgCnt - predefPosArgCnt; - if (posVarargsLength <= 0) { - varargsSeq = Constants.EMPTY_SEQUENCE; + @Override + public String getCanonicalForm() { + StringBuilder sb = new StringBuilder(); + sb.append(functionExp.getCanonicalForm()); + sb.append("("); + + boolean first = true; + if (positionalArgs != null) { + for (ASTExpression positionalArg : positionalArgs) { + if (!first) { + sb.append(", "); } else { - NativeSequence nativeSeq = new NativeSequence(posVarargsLength); - varargsSeq = nativeSeq; - for (int posVarargIdx = 0; posVarargIdx < posVarargsLength; posVarargIdx++) { - nativeSeq.add(callArgList.get(predefPosArgCnt + posVarargIdx)); - } + first = false; } - args[posVarargsIdx] = varargsSeq; + sb.append(positionalArg.getCanonicalForm()); } - - int namedVarargsArgIdx = arrayLayout.getNamedVarargsArgumentIndex(); - if (namedVarargsArgIdx != -1) { - args[namedVarargsArgIdx] = Constants.EMPTY_HASH; - } - } else { - List<TemplateModel> callArgList = arguments.getModelList(env); - args = new TemplateModel[callArgList.size()]; - for (int i = 0; i < callArgList.size(); i++) { - args[i] = callArgList.get(i); + } + if (namedArgs != null) { + for (NamedArgument namedArg : namedArgs) { + if (!first) { + sb.append(','); + } else { + first = false; + } + sb.append(_StringUtil.toFTLTopLevelIdentifierReference(namedArg.getName())); + sb.append('='); + sb.append(namedArg.getValue().getCanonicalForm()); } } - return func.execute(args, this, env); - } - - @Override - public String getCanonicalForm() { - StringBuilder buf = new StringBuilder(); - buf.append(target.getCanonicalForm()); - buf.append("("); - String list = arguments.getCanonicalForm(); - buf.append(list.substring(1, list.length() - 1)); - buf.append(")"); - return buf.toString(); + sb.append(")"); + return sb.toString(); } @Override @@ -148,24 +124,63 @@ final class ASTExpFunctionCall extends ASTExpression implements CallPlace { @Override protected ASTExpression deepCloneWithIdentifierReplaced_inner( String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ASTExpression[] positionalArgsClone; + if (positionalArgs != null) { + positionalArgsClone = new ASTExpression[positionalArgs.length]; + for (int i = 0; i < positionalArgs.length; i++) { + positionalArgsClone[i] = positionalArgs[i].deepCloneWithIdentifierReplaced( + replacedIdentifier, replacement, replacementState); + } + } else { + positionalArgsClone = null; + } + + NamedArgument[] namedArgsClone; + if (namedArgs != null) { + namedArgsClone = new NamedArgument[namedArgs.length]; + for (int i = 0; i < namedArgs.length; i++) { + NamedArgument namedArg = namedArgs[i]; + namedArgsClone[i] = new NamedArgument( + namedArg.getName(), + namedArg.getValue().deepCloneWithIdentifierReplaced( + replacedIdentifier, replacement, replacementState)); + + } + } else { + namedArgsClone = null; + } + return new ASTExpFunctionCall( - target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), - (ASTExpListLiteral) arguments.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + functionExp.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + positionalArgsClone, namedArgsClone); } @Override int getParameterCount() { - return 1 + arguments.items.size(); + return 1/*nameExp*/ + + (positionalArgs != null ? positionalArgs.length : 0) + + (namedArgs != null ? namedArgs.length * 2 : 0); } @Override Object getParameterValue(int idx) { if (idx == 0) { - return target; - } else if (idx < getParameterCount()) { - return arguments.items.get(idx - 1); + return functionExp; } else { - throw new IndexOutOfBoundsException(); + int base = 1; + final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0; + if (idx - base < positionalArgsSize) { + return positionalArgs[idx - base]; + } else { + base += positionalArgsSize; + final int namedArgsSize = namedArgs != null ? namedArgs.length : 0; + if (idx - base < namedArgsSize * 2) { + NamedArgument namedArg = namedArgs[(idx - base) / 2]; + return (idx - base) % 2 == 0 ? namedArg.getName() : namedArg.getValue(); + } else { + throw new IndexOutOfBoundsException(); + } + } } } @@ -173,10 +188,20 @@ final class ASTExpFunctionCall extends ASTExpression implements CallPlace { ParameterRole getParameterRole(int idx) { if (idx == 0) { return ParameterRole.CALLEE; - } else if (idx < getParameterCount()) { - return ParameterRole.ARGUMENT_VALUE; } else { - throw new IndexOutOfBoundsException(); + int base = 1; + final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0; + if (idx - base < positionalArgsSize) { + return ParameterRole.ARGUMENT_VALUE; + } else { + base += positionalArgsSize; + final int namedArgsSize = namedArgs != null ? namedArgs.length : 0; + if (idx - base < namedArgsSize * 2) { + return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE; + } else { + throw new IndexOutOfBoundsException(); + } + } } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core/src/main/java/org/apache/freemarker/core/_CallableUtils.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_CallableUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_CallableUtils.java index 4ab59b4..89bf91b 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/_CallableUtils.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_CallableUtils.java @@ -22,15 +22,21 @@ package org.apache.freemarker.core; import java.io.IOException; import java.io.Serializable; import java.io.Writer; +import java.util.Collection; import java.util.List; import org.apache.freemarker.core.model.ArgumentArrayLayout; import org.apache.freemarker.core.model.Constants; +import org.apache.freemarker.core.model.TemplateCallableModel; import org.apache.freemarker.core.model.TemplateDirectiveModel; import org.apache.freemarker.core.model.TemplateFunctionModel; 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.model.TemplateSequenceModel; +import org.apache.freemarker.core.util.FTLUtil; +import org.apache.freemarker.core.util.StringToIndexMap; +import org.apache.freemarker.core.util._CollectionUtil; /** * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! @@ -165,5 +171,177 @@ public final class _CallableUtils { } throw new NonStringException((Serializable) argName != null ? argName : argIndex, argValue, null, null); } - + + static TemplateModel[] getExecuteArgs( + ASTExpression[] positionalArgs, NamedArgument[] namedArgs, ArgumentArrayLayout argsLayout, + TemplateCallableModel callableValue, + Environment env) throws + TemplateException { + return argsLayout != null + ? getExecuteArgsBasedOnLayout(positionalArgs, namedArgs, argsLayout, callableValue, env) + : getExecuteArgsWithoutLayout(positionalArgs, namedArgs, callableValue, env); + } + + private static TemplateModel[] getExecuteArgsWithoutLayout(ASTExpression[] positionalArgs, + NamedArgument[] namedArgs, TemplateCallableModel callableValue, Environment env) + throws TemplateException { + if (namedArgs != null) { + throw new _MiscTemplateException(env, getNamedArgumentsNotSupportedMessage(callableValue, namedArgs[0])); + } + + TemplateModel[] execArgs; + if (positionalArgs != null) { + execArgs = new TemplateModel[positionalArgs.length]; + for (int i = 0; i < positionalArgs.length; i++) { + ASTExpression positionalArg = positionalArgs[i]; + execArgs[i] = positionalArg.eval(env); + } + } else { + execArgs = Constants.EMPTY_TEMPLATE_MODEL_ARRAY; + } + return execArgs; + } + + private static TemplateModel[] getExecuteArgsBasedOnLayout( + ASTExpression[] positionalArgs, NamedArgument[] namedArgs, ArgumentArrayLayout argsLayout, + TemplateCallableModel callableValue, + Environment env) throws TemplateException { + int predefPosArgCnt = argsLayout.getPredefinedPositionalArgumentCount(); + int posVarargsArgIdx = argsLayout.getPositionalVarargsArgumentIndex(); + + TemplateModel[] execArgs = new TemplateModel[argsLayout.getTotalLength()]; + + // Fill predefined positional args: + if (positionalArgs != null) { + int actualPredefPosArgCnt = Math.min(positionalArgs.length, predefPosArgCnt); + for (int argIdx = 0; argIdx < actualPredefPosArgCnt; argIdx++) { + execArgs[argIdx] = positionalArgs[argIdx].eval(env); + } + } + + if (posVarargsArgIdx != -1) { + int posVarargsLength = positionalArgs != null ? positionalArgs.length - predefPosArgCnt : 0; + TemplateSequenceModel varargsSeq; + if (posVarargsLength <= 0) { + varargsSeq = Constants.EMPTY_SEQUENCE; + } else { + NativeSequence nativeSeq = new NativeSequence(posVarargsLength); + varargsSeq = nativeSeq; + for (int posVarargIdx = 0; posVarargIdx < posVarargsLength; posVarargIdx++) { + nativeSeq.add(positionalArgs[predefPosArgCnt + posVarargIdx].eval(env)); + } + } + execArgs[posVarargsArgIdx] = varargsSeq; + } else if (positionalArgs != null && positionalArgs.length > predefPosArgCnt) { + checkSupportsAnyParameters(callableValue, argsLayout, env); + List<String> validPredefNames = argsLayout.getPredefinedNamedArgumentsMap().getKeys(); + _ErrorDescriptionBuilder errorDesc = new _ErrorDescriptionBuilder( + "The called ", FTLUtil.getCallableTypeName(callableValue), " ", + (predefPosArgCnt != 0 + ? new Object[]{ "can only have ", predefPosArgCnt } + : "can't have" + ), + " arguments passed by position, but the invocation has ", + positionalArgs.length, " such arguments.", + (!argsLayout.isPositionalParametersSupported() && argsLayout.isNamedParametersSupported() ? + new Object[] { + " Try to pass arguments by name (as in ", + (callableValue instanceof TemplateDirectiveModel + ? "<@example x=1 y=2 />" + : "example(x=1, y=2)"), + ")", + (!validPredefNames.isEmpty() + ? new Object[] { " The supported parameter names are:\n", + new _DelayedJQuotedListing(validPredefNames)} + : _CollectionUtil.EMPTY_OBJECT_ARRAY)} + : "") + ); + if (callableValue instanceof Environment.TemplateLanguageDirective + && !argsLayout.isPositionalParametersSupported() && argsLayout.isNamedParametersSupported()) { + errorDesc.tip("You can pass a parameter by position (i.e., without specifying its name, as you" + + " have tried now) when the macro has defined that parameter to be a positional parameter. " + + "See in the documentation how, and when that's a good practice."); + } + throw new _MiscTemplateException(env, + errorDesc + ); + } + + int namedVarargsArgumentIndex = argsLayout.getNamedVarargsArgumentIndex(); + NativeHashEx2 namedVarargsHash = null; + if (namedArgs != null) { + StringToIndexMap predefNamedArgsMap = argsLayout.getPredefinedNamedArgumentsMap(); + for (NamedArgument namedArg : namedArgs) { + int argIdx = predefNamedArgsMap.get(namedArg.name); + if (argIdx != -1) { + execArgs[argIdx] = namedArg.value.eval(env); + } else { + if (namedVarargsHash == null) { + if (namedVarargsArgumentIndex == -1) { + checkSupportsAnyParameters(callableValue, argsLayout, env); + Collection<String> validNames = predefNamedArgsMap.getKeys(); + throw new _MiscTemplateException(env, + validNames == null || validNames.isEmpty() + ? getNamedArgumentsNotSupportedMessage(callableValue, namedArg) + : new Object[] { + "The called ", FTLUtil.getCallableTypeName(callableValue), + " has no parameter that's passed by name and is called ", + new _DelayedJQuote(namedArg.name), + ". The supported parameter names are:\n", + new _DelayedJQuotedListing(validNames) + }); + } + + namedVarargsHash = new NativeHashEx2(); + } + namedVarargsHash.put(namedArg.name, namedArg.value.eval(env)); + } + } + } + if (namedVarargsArgumentIndex != -1) { + execArgs[namedVarargsArgumentIndex] = namedVarargsHash != null ? namedVarargsHash : Constants.EMPTY_HASH; + } + return execArgs; + } + + private static Object[] getNamedArgumentsNotSupportedMessage(TemplateCallableModel callableValue, + NamedArgument namedArg) { + return new Object[] { + "The called ", FTLUtil.getCallableTypeName(callableValue), + " can't have arguments that are passed by name (like ", + new _DelayedJQuote(namedArg.name), "). Try to pass arguments by position " + + "(i.e, without name, as in ", + (callableValue instanceof TemplateDirectiveModel + ? "<@example arg1, arg2, arg3 />" + : "example(arg1, arg2, arg3)"), + ")." + }; + } + + static private void checkSupportsAnyParameters( + TemplateCallableModel callableValue, ArgumentArrayLayout argsLayout, Environment env) + throws TemplateException { + if (argsLayout.getTotalLength() == 0) { + throw new _MiscTemplateException(env, + "The called ", FTLUtil.getCallableTypeName(callableValue), " doesn't support any parameters."); + } + } + + static final class NamedArgument { + private final String name; + private final ASTExpression value; + + NamedArgument(String name, ASTExpression value) { + this.name = name; + this.value = value; + } + + String getName() { + return name; + } + + ASTExpression getValue() { + return value; + } + } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/freemarker-core/src/main/java/org/apache/freemarker/core/model/ArgumentArrayLayout.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/ArgumentArrayLayout.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ArgumentArrayLayout.java index e974570..cc63aaf 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/ArgumentArrayLayout.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/ArgumentArrayLayout.java @@ -243,4 +243,18 @@ public final class ArgumentArrayLayout { return arrayLength; } + /** + * Tells if there can be any positional parameters (predefined or varargs). + */ + public boolean isPositionalParametersSupported() { + return getPredefinedPositionalArgumentCount() != 0 || getPositionalVarargsArgumentIndex() != -1; + } + + /** + * Tells if there can be any named parameters (predefined or varargs). + */ + public boolean isNamedParametersSupported() { + return getPredefinedNamedArgumentsMap().size() != 0 || getNamedVarargsArgumentIndex() != -1; + } + } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b59a03a8/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 4201d84..2ae31c0 100644 --- a/freemarker-core/src/main/javacc/FTL.jj +++ b/freemarker-core/src/main/javacc/FTL.jj @@ -35,7 +35,7 @@ import org.apache.freemarker.core.outputformat.impl.*; import org.apache.freemarker.core.model.*; import org.apache.freemarker.core.model.impl.*; import org.apache.freemarker.core.util.*; -import org.apache.freemarker.core.ASTDynamicTopLevelCall.NamedArgument; +import org.apache.freemarker.core._CallableUtils.NamedArgument; import java.io.*; import java.util.*; import java.nio.charset.Charset; @@ -1838,7 +1838,7 @@ ASTExpression AddSubExpression(ASTExpression exp) : | result = DynamicKey(exp) | - result = MethodArgs(exp) + result = FunctionCall(exp) | result = ASTExpBuiltIn(exp) | @@ -2038,25 +2038,115 @@ ASTExpression DynamicKey(ASTExpression exp) : } /** - * production for an arglist part of a method invocation. + * This is the argument list part of a function/method invocation. */ -ASTExpFunctionCall MethodArgs(ASTExpression exp) : +ASTExpFunctionCall FunctionCall(ASTExpression functionExp) : { - ArrayList args = new ArrayList(); - Token end; + Token t; + ASTExpression exp; + + Token start = null, end; + Token prevChoiceComma = null; + List<ASTExpression> positionalArgs = null; + List<NamedArgument> namedArgs = null; } { - <OPEN_PAREN> - args = PositionalArgs() - end = <CLOSE_PAREN> - { - args.trimToSize(); - ASTExpFunctionCall result = new ASTExpFunctionCall(exp, args); - result.setLocation(template, exp, end); - return result; + start = <OPEN_PAREN> + + ( + LOOKAHEAD(<ID><ASSIGNMENT_EQUALS>) + ( + t = <ID> + <ASSIGNMENT_EQUALS> + exp = ASTExpression() + { + if (prevChoiceComma == null && (positionalArgs != null || namedArgs != null)) { + throw new ParseException("Missing comma (\",\") before expression. " + + "Function call arguments must be separated by comma (even named arguments).", exp); + } + prevChoiceComma = null; + + if (namedArgs == null) { + namedArgs = new ArrayList<NamedArgument>(8); + } + namedArgs.add(new NamedArgument(t.image, exp)); + } + ) + | + ( + // This was made to be choice its own, as we can give better error messages this way. + t = <COMMA> + { + if (prevChoiceComma != null) { + throw new ParseException("Two commas (\",\") without argument between.", + template, t); + } + prevChoiceComma = t; + + if (namedArgs == null && positionalArgs == null) { + throw new ParseException( + "Remove comma (\",\"); it can only be used between arguments.", + template, t); + } + } + ) + | + ( + exp = ASTExpression() + { + if (namedArgs != null) { + throw new ParseException( + "Expression looks like an argument passed by position (no preceding \"<argName>=\"), " + + " but those must be earlier than arguments passed by name.", + exp); + } + + if (prevChoiceComma == null && (positionalArgs != null || namedArgs != null)) { + throw new ParseException("Missing comma (\",\") before expression. " + + "Function call arguments must be separated by comma.", exp); + } + prevChoiceComma = null; + + if (positionalArgs == null) { + positionalArgs = new ArrayList<ASTExpression>(8); + } + positionalArgs.add(exp); + } + ) + )* + + end = <CLOSE_PAREN> + + { + ASTExpression[] trimmedPositionalArgs; + if (positionalArgs == null) { + trimmedPositionalArgs = null; + } else { + int len = positionalArgs.size(); + trimmedPositionalArgs = new ASTExpression[len]; + for (int i = 0; i < len; i++) { + trimmedPositionalArgs[i] = positionalArgs.get(i); + } } + + NamedArgument[] trimmedNamedArgs; + if (namedArgs == null) { + trimmedNamedArgs = null; + } else { + int len = namedArgs.size(); + trimmedNamedArgs = new NamedArgument[len]; + for (int i = 0; i < len; i++) { + trimmedNamedArgs[i] = namedArgs.get(i); + } + } + + ASTExpFunctionCall result = new ASTExpFunctionCall(functionExp, trimmedPositionalArgs, trimmedNamedArgs); + result.setLocation(template, start, end); + return result; + } } + ASTExpStringLiteral ASTExpStringLiteral(boolean interpolate) : { Token t;