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
commit 64375a58ee3ec6af67f318f389d3eeb8279db64e Author: Gary Gregory <[email protected]> AuthorDate: Mon Oct 12 14:25:18 2026 -0400 Expand AnnotationUtils nested annotation array hash tests Make the regression test deterministic by returning a deliberately incorrect hash instead of an identity hash. Add coverage for empty arrays, multiple elements, element order, null elements, and deeper nesting. Clarify the helper Javadoc to describe recursive element hashing and Arrays.hashCode mixing. --- .../org/apache/commons/lang3/AnnotationUtils.java | 4 +- .../apache/commons/lang3/AnnotationUtilsTest.java | 62 +++++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java index 0554e5529..1d9f95fd2 100644 --- a/src/main/java/org/apache/commons/lang3/AnnotationUtils.java +++ b/src/main/java/org/apache/commons/lang3/AnnotationUtils.java @@ -118,7 +118,9 @@ private static boolean annotationArrayMemberEquals(final Annotation[] a1, final } /** - * Helper method for generating a hash code for an array of annotations. + * Generates an array hash code using {@link #hashCode(Annotation)} for each non-null element. + * + * <p>Uses the element order, initial value, multiplier, and null-element handling of {@link Arrays#hashCode(Object[])}.</p> * * @param annotations The array * @return A hash code for the specified array diff --git a/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java b/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java index bddf44f46..ef5162814 100644 --- a/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java @@ -32,6 +32,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.time.Duration; +import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.function.Function; @@ -50,6 +51,21 @@ class AnnotationUtilsTest extends AbstractLangTest { private static final class AnnotatedClass { } + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationArray { + FunctionAnnotation[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationArrayContainer { + AnnotationArray[] value(); + } + + @AnnotationArray({ @FunctionAnnotation("first"), @FunctionAnnotation("second") }) + @AnnotationArrayContainer(@AnnotationArray({ @FunctionAnnotation("first"), @FunctionAnnotation("second") })) + private static final class ArrayAnnotatedClass { + } + @Retention(RetentionPolicy.RUNTIME) public @interface FunctionAnnotation { Function<String, String> FUNCTION = value -> value; @@ -526,6 +542,48 @@ void testHashCode() { }); } + @Test + void testHashCodeOfAnnotationArrayEdgeCases() { + final AnnotationArray real = ArrayAnnotatedClass.class.getAnnotation(AnnotationArray.class); + final FunctionAnnotation first = real.value()[0]; + final FunctionAnnotation second = real.value()[1]; + final FunctionAnnotation[][] values = { + {}, { first }, { first, second }, { second, first }, { null }, { first, null, second } + }; + for (final FunctionAnnotation[] value : values) { + final AnnotationArray generated = (AnnotationArray) Proxy.newProxyInstance( + AnnotationArray.class.getClassLoader(), new Class<?>[] { AnnotationArray.class }, + (proxy, method, args) -> "value".equals(method.getName()) ? value : method.invoke(real, args)); + final int expected = 127 * "value".hashCode() ^ Arrays.hashCode(value); + assertEquals(expected, AnnotationUtils.hashCode(generated)); + } + } + + @Test + void testHashCodeOfDeeplyNestedAnnotationArray() { + final AnnotationArrayContainer real = ArrayAnnotatedClass.class.getAnnotation(AnnotationArrayContainer.class); + final AnnotationArray inner = real.value()[0]; + final FunctionAnnotation[] values = inner.value(); + final FunctionAnnotation[] generatedValues = new FunctionAnnotation[values.length]; + for (int i = 0; i < values.length; i++) { + final FunctionAnnotation value = values[i]; + generatedValues[i] = (FunctionAnnotation) Proxy.newProxyInstance( + FunctionAnnotation.class.getClassLoader(), new Class<?>[] { FunctionAnnotation.class }, + (proxy, method, args) -> "hashCode".equals(method.getName()) + ? Integer.valueOf(value.hashCode() + 1) : method.invoke(value, args)); + } + final AnnotationArray generatedInner = (AnnotationArray) Proxy.newProxyInstance( + AnnotationArray.class.getClassLoader(), new Class<?>[] { AnnotationArray.class }, + (proxy, method, args) -> "value".equals(method.getName()) ? generatedValues : method.invoke(inner, args)); + final AnnotationArrayContainer generated = (AnnotationArrayContainer) Proxy.newProxyInstance( + AnnotationArrayContainer.class.getClassLoader(), new Class<?>[] { AnnotationArrayContainer.class }, + (proxy, method, args) -> "value".equals(method.getName()) + ? new AnnotationArray[] { generatedInner } : method.invoke(real, args)); + assertTrue(AnnotationUtils.equals(real, generated)); + assertTrue(AnnotationUtils.equals(generated, real)); + assertEquals(real.hashCode(), AnnotationUtils.hashCode(generated)); + } + @Test void testHashCodeOfNestedAnnotationArray() { assertTimeoutPreemptively(Duration.ofSeconds(666L), () -> { @@ -533,7 +591,7 @@ void testHashCodeOfNestedAnnotationArray() { // A nested annotation whose own hashCode() does not follow Annotation#hashCode(). final InvocationHandler nestHandler = (proxy, method, args) -> { if ("hashCode".equals(method.getName()) && method.getParameterTypes().length == 0) { - return Integer.valueOf(System.identityHashCode(proxy)); + return Integer.valueOf(real.nests()[0].hashCode() + 1); } return method.invoke(real.nests()[0], args); }; @@ -563,7 +621,7 @@ void testHashCodeWithFunction() { void testHashCodeWithoutInvokingSupplier() { final ThrowingSupplierAnnotation annotation = AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class); - final int expected = (127 * "value".hashCode()) ^ "member".hashCode(); + final int expected = 127 * "value".hashCode() ^ "member".hashCode(); assertEquals(expected, AnnotationUtils.hashCode(annotation)); }
