This is an automated email from the ASF dual-hosted git repository. benweidig pushed a commit to branch property-expression-parser in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 98e909b112140bfaa5db91e5ccb66a98abe8b47a Author: Ben Weidig <[email protected]> AuthorDate: Sun Oct 25 12:58:44 2026 +0100 TAP5-2842: Prefer declared methods over bridge methods in property expressions A method invocation used the first method of Class.getMethods() with a matching name and parameter count. That order is unspecified, so a method implementing a generic interface could resolve to its bridge method, and the conduit got the erased type Object instead of the declared return type. A bridge method is now used only when nothing else matches. A public method inherited from a non-public class is reachable through a bridge method alone. --- .../services/PropertyConduitSourceImpl.java | 16 +++- .../tapestry5/beanmodel/BridgeMethodTest.java | 87 ++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java index 7b2e13682..e7ceb5eb5 100644 --- a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java +++ b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java @@ -1310,14 +1310,28 @@ public class PropertyConduitSourceImpl implements PropertyConduitSource while (true) { + // getMethods() returns them in no particular order. A bridge method has an erased return type, + // so it is used only when nothing else matches: javac makes a public method inherited from a + // non-public class reachable through a bridge alone. + + Method bridge = null; for (Method method : searchType.getMethods()) { if (method.getParameterTypes().length == parameterCount && method.getName().equalsIgnoreCase(methodName)) - return method; + { + if (!method.isBridge()) + return method; + + if (bridge == null) + bridge = method; + } } + if (bridge != null) + return bridge; + // TAP5-330 if (searchType != Object.class) { diff --git a/beanmodel/src/test/java/org/apache/tapestry5/beanmodel/BridgeMethodTest.java b/beanmodel/src/test/java/org/apache/tapestry5/beanmodel/BridgeMethodTest.java new file mode 100644 index 000000000..93616a355 --- /dev/null +++ b/beanmodel/src/test/java/org/apache/tapestry5/beanmodel/BridgeMethodTest.java @@ -0,0 +1,87 @@ +// Licensed 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.tapestry5.beanmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.apache.tapestry5.beanmodel.services.PropertyConduitSource; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * A method invocation resolves to the declared method, not to a compiler-generated bridge method, + * whatever order {@link Class#getMethods()} returns them in. + */ +public class BridgeMethodTest +{ + private static PropertyConduitSource source; + + @BeforeAll + static void setup() + { + source = PropertyConduitSourceFixture.create(); + } + + @Test + void generic_interface_method_has_the_resolved_return_type() + { + assertSame(Long.class, source.create(LongProducer.class, "produce()").getPropertyType()); + } + + @Test + void generic_interface_method_is_invoked() + { + assertEquals(42L, source.create(LongProducer.class, "produce()").get(new LongProducer())); + } + + @Test + void method_inherited_from_package_private_class_is_invoked() + { + // javac makes the inherited method reachable only through a bridge method in the public subclass. + + assertEquals("base", source.create(PublicSubclass.class, "getValue()").get(new PublicSubclass())); + } + + @Test + void method_inherited_from_package_private_class_has_its_return_type() + { + assertSame(String.class, source.create(PublicSubclass.class, "getValue()").getPropertyType()); + } + + public interface Producer<T> + { + T produce(); + } + + public static class LongProducer implements Producer<Long> + { + @Override + public Long produce() + { + return 42L; + } + } + + static class PackagePrivateBase + { + public String getValue() + { + return "base"; + } + } + + public static class PublicSubclass extends PackagePrivateBase + { + } +}
