This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new daa17b862 Annotation lookup matches unrelated overloads via
assignable-parameter resolution (#1637)
daa17b862 is described below
commit daa17b8629a7a9921ffe42e7c2c0f0051af42571
Author: Gary Gregory <[email protected]>
AuthorDate: Thu May 7 07:21:57 2026 -0400
Annotation lookup matches unrelated overloads via assignable-parameter
resolution (#1637)
MethodUtils.getAnnotation(Method, Class<A>, boolean, boolean)
---
.../apache/commons/lang3/reflect/MethodUtils.java | 51 ++++++++++++++-
.../lang3/reflect/MethodUtilsAnnotationsTest.java | 76 ++++++++++++++++++++++
2 files changed, 124 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
index df6899f5a..a3b734380 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
@@ -272,11 +272,56 @@ public static <A extends Annotation> A
getAnnotation(final Method method, final
A annotation = method.getAnnotation(annotationCls);
if (annotation == null && searchSupers) {
final Class<?> mcls = method.getDeclaringClass();
+ final String methodName = method.getName();
+ final Class<?>[] paramTypes = method.getParameterTypes();
final List<Class<?>> classes =
getAllSuperclassesAndInterfaces(mcls);
for (final Class<?> acls : classes) {
- final Method equivalentMethod = ignoreAccess ?
getMatchingMethod(acls, method.getName(), method.getParameterTypes())
- : getMatchingAccessibleMethod(acls, method.getName(),
method.getParameterTypes());
- if (equivalentMethod != null) {
+ // First, attempt an exact parameter-type match
(getDeclaredMethod) to
+ // find a true override. This avoids matching unrelated
overloads that
+ // are merely assignable-compatible (e.g. process(Integer) vs
+ // process(Number)).
+ Method equivalentMethod = null;
+ try {
+ equivalentMethod = acls.getDeclaredMethod(methodName,
paramTypes);
+ } catch (final NoSuchMethodException ignored) {
+ // No exact match; check for generic-bridge scenario: the
declaring
+ // class may use a type variable whose erased form is
Object (or
+ // another bound). In that case the parent method's erased
+ // parameter types differ from the child's concrete types,
so we
+ // scan declared methods for a same-name method whose
*erased*
+ // parameter count matches and whose erased types are
assignable
+ // from our concrete types.
+ for (final Method candidate : acls.getDeclaredMethods()) {
+ if (!candidate.getName().equals(methodName)) {
+ continue;
+ }
+ final Class<?>[] candidateParams =
candidate.getParameterTypes();
+ if (candidateParams.length != paramTypes.length) {
+ continue;
+ }
+ // Require that every concrete param type is
assignable to the
+ // candidate's (erased) param type AND that the
candidate is
+ // generic (has at least one TypeVariable in its
generic
+ // parameter types). This prevents matching plain
overloads.
+ boolean genericMatch = false;
+ boolean paramsMatch = true;
+ final java.lang.reflect.Type[] genericParams =
candidate.getGenericParameterTypes();
+ for (int i = 0; i < candidateParams.length; i++) {
+ if (genericParams[i] instanceof
java.lang.reflect.TypeVariable) {
+ genericMatch = true;
+ }
+ if (!ClassUtils.isAssignable(paramTypes[i],
candidateParams[i], true)) {
+ paramsMatch = false;
+ break;
+ }
+ }
+ if (paramsMatch && genericMatch) {
+ equivalentMethod = candidate;
+ break;
+ }
+ }
+ }
+ if (equivalentMethod != null && (ignoreAccess ||
MemberUtils.isAccessible(equivalentMethod))) {
annotation = equivalentMethod.getAnnotation(annotationCls);
if (annotation != null) {
break;
diff --git
a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsAnnotationsTest.java
b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsAnnotationsTest.java
new file mode 100644
index 000000000..caaa1cbf9
--- /dev/null
+++
b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsAnnotationsTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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
+ *
+ * https://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.commons.lang3.reflect;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link MethodUtils#getAnnotation(Method, Class, boolean, boolean)}.
+ * <p>
+ * getMatchingMethod allows assignable params, potentially finding annotations
on unrelated overloads.
+ * </p>
+ */
+public class MethodUtilsAnnotationsTest {
+
+ /** Interface with a method taking Number, annotated @Deprecated */
+ public interface Processor {
+
+ @SuppressWarnings("javadoc")
+ @Deprecated
+ void process(Number n);
+ }
+
+ /** Implementation that does NOT annotate process(Integer) */
+ public static class ProcessorImpl implements Processor {
+
+ // Overload with Integer — NOT annotated
+ @SuppressWarnings("javadoc")
+ public void process(final Integer i) {
+ // intentionally no @Deprecated
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void process(final Number n) {
+ // inherited, annotated on interface
+ }
+ }
+
+ /**
+ * getAnnotation() for process(Integer) should return null because the
Integer overload is NOT an override of process(Number).
+ * <ul>
+ * <li>Pre-patch: getMatchingMethod finds process(Number) (since Integer
is assignable to Number) and returns the {@code @Deprecated} annotation
+ * incorrectly.</li>
+ * <li>Post-patch: uses getDeclaredMethod with exact types, finds nothing,
returns null.</li>
+ * </ul>
+ */
+ @SuppressWarnings("javadoc")
+ @Test
+ public void testAnnotationLookupDoesNotMatchAssignableOverload() throws
NoSuchMethodException {
+ final Method integerMethod =
ProcessorImpl.class.getDeclaredMethod("process", Integer.class);
+ final Deprecated ann = MethodUtils.getAnnotation(integerMethod,
Deprecated.class, true, true);
+ assertNull(ann, "process(Integer) is NOT an override of
process(Number); its annotation lookup must return null");
+ final Method numberMethod =
ProcessorImpl.class.getDeclaredMethod("process", Number.class);
+ assertNotNull(MethodUtils.getAnnotation(numberMethod,
Deprecated.class, true, true));
+ }
+}