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 e588cc629 [LANG-1835] Ignore synthetic methods in AnnotationUtils 
(#1810)
e588cc629 is described below

commit e588cc629f4b046cdf0ab2fe4d8ab8e297d8a484
Author: 안승현 <[email protected]>
AuthorDate: Sun Oct 11 02:31:47 2026 +0900

    [LANG-1835] Ignore synthetic methods in AnnotationUtils (#1810)
    
    * [LANG-1835] Ignore synthetic methods in AnnotationUtils
    
    Only process public, abstract, non-synthetic annotation members
    in equals(), hashCode(), and toString().
    
    Prevent compiler-generated lambda methods from being invoked
    or affecting annotation hash codes and string representations.
    
    Add regression tests for Runnable, Supplier, and Function constants,
    including equality checks between distinct annotation instances.
    
    Validate with the default Maven build on JDK 25.
    
    Signed-off-by: 안승현 <[email protected]>
    
    * [LANG-1835] Fix annotation equality test on older JDKs
    
    Remove the native annotation proxy equals() assertion that triggers
    AnnotationFormatError on Java 8 and 11.
    
    Retain the distinct-instance check and the AnnotationUtils.equals()
    regression assertion.
    
    Validate the default Maven build with -Ddoclint=all on Windows
    with Java 8, 11, 17, 21, 25, and 27.
    
    Signed-off-by: 안승현 <[email protected]>
    
    * [LANG-1835] Consolidate annotation member validation
    
    Move parameter count and return type checks into isAnnotationMember()
    and simplify the equals() and toString() call sites.
    
    Retain reflective access support for non-public annotation types.
    
    Validate the full default Maven build on Java 8, 11, 17, 21, 25, and 27.
    
    Signed-off-by: 안승현 <[email protected]>
    
    * [LANG-1835] Ensure distinct instances in annotation equality test
    
    Create a fresh proxy instead of relying on annotation instance caching.
    Retain the identity assertion to ensure the regression exercises member 
comparison.
    
    Signed-off-by: 안승현 [email protected]>
    Signed-off-by: 안승현 <[email protected]>
    
    * Add test for AnnotationUtils with throwing supplier
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * [LANG-1835] Add hashCode regression test for throwing supplier
    
    Verify that AnnotationUtils.hashCode() returns the expected value
    without invoking synthetic lambda-body methods.
    
    Signed-off-by: 안승현 <[email protected]>
    
    * [LANG-1835] Override annotation access only when needed
    
    Invoke annotation members normally first and adjust accessibility only
    after IllegalAccessException. Preserve Java 8 compatibility, non-public
    annotation support, and the existing forceAccessible policy.
    
    Signed-off-by: 안승현 <[email protected]>
    
    ---------
    
    Signed-off-by: 안승현 <[email protected]>
    Co-authored-by: Gary Gregory <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../org/apache/commons/lang3/AnnotationUtils.java  |  55 ++++++++---
 .../apache/commons/lang3/AnnotationUtilsTest.java  | 103 ++++++++++++++++++++-
 2 files changed, 146 insertions(+), 12 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java 
b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java
index 9ec3a58b5..bf6cd4756 100644
--- a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java
+++ b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java
@@ -18,6 +18,7 @@
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.util.Arrays;
 
 import org.apache.commons.lang3.builder.AbstractReflection;
@@ -217,11 +218,9 @@ public static boolean equals(final Annotation a1, final 
Annotation a2) {
         }
         try {
             for (final Method m : type1.getDeclaredMethods()) {
-                if (m.getParameterTypes().length == 0
-                        && isValidAnnotationMemberType(m.getReturnType())) {
-                    
AbstractReflection.setAccessible(AbstractReflection.getForceAccessible(), m);
-                    final Object v1 = m.invoke(a1);
-                    final Object v2 = m.invoke(a2);
+                if (isAnnotationMember(m)) {
+                    final Object v1 = invokeAnnotationMember(m, a1);
+                    final Object v2 = invokeAnnotationMember(m, a2);
                     if (!memberEquals(m.getReturnType(), v1, v2)) {
                         return false;
                     }
@@ -247,9 +246,11 @@ public static int hashCode(final Annotation a) {
         int result = 0;
         final Class<? extends Annotation> type = a.annotationType();
         for (final Method m : type.getDeclaredMethods()) {
+            if (!isAnnotationMember(m)) {
+                continue;
+            }
             try {
-                
AbstractReflection.setAccessible(AbstractReflection.getForceAccessible(), m);
-                final Object value = m.invoke(a);
+                final Object value = invokeAnnotationMember(m, a);
                 if (value == null) {
                     throw new IllegalStateException(String.format("Annotation 
method %s returned null", m));
                 }
@@ -280,6 +281,39 @@ private static int hashMember(final String name, final 
Object value) {
         return part1 ^ value.hashCode();
     }
 
+    /**
+     * Invokes an annotation member, enabling reflective access only when 
needed.
+     *
+     * @param method the annotation member method
+     * @param annotation the annotation instance
+     * @return the annotation member value
+     * @throws ReflectiveOperationException if the method cannot be invoked
+     */
+    private static Object invokeAnnotationMember(final Method method, final 
Annotation annotation)
+            throws ReflectiveOperationException {
+        try {
+            return method.invoke(annotation);
+        } catch (final IllegalAccessException ex) {
+            if 
(!AbstractReflection.setAccessible(AbstractReflection.getForceAccessible(), 
method)) {
+                throw ex;
+            }
+            return method.invoke(annotation);
+        }
+    }
+
+    /**
+     * Tests whether the specified method declares an annotation member.
+     *
+     * @param method The method to check
+     * @return whether the method is public, abstract, non-synthetic, has no 
parameters,
+     *         and has a valid annotation member return type
+     */
+    private static boolean isAnnotationMember(final Method method) {
+        final int modifiers = method.getModifiers();
+        return Modifier.isPublic(modifiers) && Modifier.isAbstract(modifiers) 
&& !method.isSynthetic()
+                && method.getParameterCount() == 0 && 
isValidAnnotationMemberType(method.getReturnType());
+    }
+
     /**
      * Tests whether the specified type is permitted as an annotation member.
      *
@@ -341,12 +375,11 @@ private static boolean memberEquals(final Class<?> type, 
final Object o1, final
     public static String toString(final Annotation a) {
         final ToStringBuilder builder = new ToStringBuilder(a, 
TO_STRING_STYLE);
         for (final Method m : a.annotationType().getDeclaredMethods()) {
-            if (m.getParameterTypes().length > 0) {
-                continue; // what?
+            if (!isAnnotationMember(m)) {
+                continue;
             }
             try {
-                
AbstractReflection.setAccessible(AbstractReflection.getForceAccessible(), m);
-                builder.append(m.getName(), m.invoke(a));
+                builder.append(m.getName(), invokeAnnotationMember(m, a));
             } catch (final ReflectiveOperationException ex) {
                 throw new UncheckedException(ex);
             }
diff --git a/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java
index d8a1044fc..3bc12fcff 100644
--- a/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java
@@ -19,6 +19,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -33,6 +34,8 @@
 import java.time.Duration;
 import java.util.Collection;
 import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -40,6 +43,45 @@
 /**
  */
 class AnnotationUtilsTest extends AbstractLangTest {
+    @FunctionAnnotation
+    @RunnableAnnotation
+    @SupplierAnnotation
+    @ThrowingSupplierAnnotation
+    private static final class AnnotatedClass {
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface FunctionAnnotation {
+        Function<String, String> FUNCTION = value -> value;
+
+        String value() default "member";
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface RunnableAnnotation {
+        Runnable RUNNABLE = () -> {
+            // Creates a void synthetic method for the regression test.
+        };
+
+        String value() default "member";
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface SupplierAnnotation {
+        Supplier<String> SUPPLIER = () -> "constant";
+
+        String value() default "member";
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface ThrowingSupplierAnnotation {
+        Supplier<String> SUPPLIER = () -> {
+            throw new AssertionError("Annotation member access must not invoke 
a lambda");
+        };
+
+        String value() default "member";
+    }
+
     @Retention(RetentionPolicy.RUNTIME)
     public @interface NestAnnotation {
         boolean booleanValue();
@@ -397,7 +439,7 @@ final class None extends Throwable {
     private Field field4;
 
     @BeforeEach
-    public void setup() throws Exception {
+    void setup() throws Exception {
         field1 = getClass().getDeclaredField("dummy1");
         field2 = getClass().getDeclaredField("dummy2");
         field3 = getClass().getDeclaredField("dummy3");
@@ -421,6 +463,20 @@ void testEquivalence() {
         
assertTrue(AnnotationUtils.equals(field2.getAnnotation(TestAnnotation.class), 
field1.getAnnotation(TestAnnotation.class)));
     }
 
+    @Test
+    void testEquivalenceWithoutInvokingSupplier() {
+        final ThrowingSupplierAnnotation first =
+                
AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
+        final ThrowingSupplierAnnotation second =
+                (ThrowingSupplierAnnotation) Proxy.newProxyInstance(
+                        ThrowingSupplierAnnotation.class.getClassLoader(),
+                        new Class<?>[] { ThrowingSupplierAnnotation.class },
+                        (proxy, method, args) -> method.invoke(first, args));
+
+        assertNotSame(first, second);
+        assertTrue(AnnotationUtils.equals(first, second));
+    }
+
     @Test
     void testGeneratedAnnotationEquivalentToRealAnnotation() {
         assertTimeoutPreemptively(Duration.ofSeconds(666L), () -> {
@@ -470,6 +526,33 @@ void testHashCode() {
         });
     }
 
+    @Test
+    void testHashCodeWithFunction() {
+        final FunctionAnnotation annotation = 
AnnotatedClass.class.getAnnotation(FunctionAnnotation.class);
+        assertEquals(annotation.hashCode(), 
AnnotationUtils.hashCode(annotation));
+    }
+
+    @Test
+    void testHashCodeWithRunnable() {
+        final RunnableAnnotation annotation = 
AnnotatedClass.class.getAnnotation(RunnableAnnotation.class);
+        assertEquals(annotation.hashCode(), 
AnnotationUtils.hashCode(annotation));
+    }
+
+    @Test
+    void testHashCodeWithSupplier() {
+        final SupplierAnnotation annotation = 
AnnotatedClass.class.getAnnotation(SupplierAnnotation.class);
+        assertEquals(annotation.hashCode(), 
AnnotationUtils.hashCode(annotation));
+    }
+
+    @Test
+    void testHashCodeWithoutInvokingSupplier() {
+        final ThrowingSupplierAnnotation annotation =
+                
AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
+        final int expected = (127 * "value".hashCode()) ^ "member".hashCode();
+
+        assertEquals(expected, AnnotationUtils.hashCode(annotation));
+    }
+
     @Test
     void testIsValidAnnotationMemberType() {
         for (final Class<?> type : new Class[] { byte.class, short.class, 
int.class, char.class,
@@ -519,4 +602,22 @@ void testToString() {
         });
     }
 
+    @Test
+    void testToStringWithRunnable() {
+        final RunnableAnnotation annotation = 
AnnotatedClass.class.getAnnotation(RunnableAnnotation.class);
+        assertEquals("@" + RunnableAnnotation.class.getName() + 
"(value=member)", AnnotationUtils.toString(annotation));
+    }
+
+    @Test
+    void testToStringWithSupplier() {
+        final SupplierAnnotation annotation = 
AnnotatedClass.class.getAnnotation(SupplierAnnotation.class);
+        assertEquals("@" + SupplierAnnotation.class.getName() + 
"(value=member)", AnnotationUtils.toString(annotation));
+    }
+
+    @Test
+    void testToStringWithThrowingSupplier() {
+        final ThrowingSupplierAnnotation annotation = 
AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
+        assertEquals("@" + ThrowingSupplierAnnotation.class.getName() + 
"(value=member)", AnnotationUtils.toString(annotation));
+    }
+
 }

Reply via email to