Skip to content

Commit b088d99

Browse files
committed
Apply field predicate before searching type hierarchy
Prior to this commit, findFields() and streamFields() in ReflectionSupport as well as findAnnotatedFields() and findAnnotatedFieldValues() in AnnotationSupport first searched for all fields in the type hierarchy and then applied the user-supplied predicate (or "is annotated?" predicate) afterwards. This resulted in fields in subclasses incorrectly "shadowing" package-private fields in superclasses (in a different package) even if the predicate would otherwise exclude the field in such a subclass. For example, given a superclass that declares a package-private static @⁠TempDir "tempDir" field and a subclass (in a different package) that declares a @⁠TempDir "tempDir" field, when JUnit Jupiter looked up @⁠TempDir fields for the subclass, the @⁠TempDir "tempDir" field in the superclass was not found because the @⁠TempDir "tempDir" field shadowed it based solely on the field signature, ignoring the type of annotation sought. To address that, this commit modifies the internal search algorithms in ReflectionUtils so that field predicates are applied while searching the hierarchy for fields. TODO: - add release note entries See #3498 Closes #3532
1 parent 7ff8248 commit b088d99

File tree

7 files changed

+174
-17
lines changed

7 files changed

+174
-17
lines changed

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java

+27-17
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,7 @@ public static List<Constructor<?>> findConstructors(Class<?> clazz, Predicate<Co
12381238
*/
12391239
public static List<Field> findFields(Class<?> clazz, Predicate<Field> predicate,
12401240
HierarchyTraversalMode traversalMode) {
1241+
12411242
return streamFields(clazz, predicate, traversalMode).collect(toUnmodifiableList());
12421243
}
12431244

@@ -1252,21 +1253,23 @@ public static Stream<Field> streamFields(Class<?> clazz, Predicate<Field> predic
12521253
Preconditions.notNull(predicate, "Predicate must not be null");
12531254
Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null");
12541255

1255-
return findAllFieldsInHierarchy(clazz, traversalMode).stream().filter(predicate);
1256+
return findAllFieldsInHierarchy(clazz, predicate, traversalMode).stream();
12561257
}
12571258

1258-
private static List<Field> findAllFieldsInHierarchy(Class<?> clazz, HierarchyTraversalMode traversalMode) {
1259+
private static List<Field> findAllFieldsInHierarchy(Class<?> clazz, Predicate<Field> predicate,
1260+
HierarchyTraversalMode traversalMode) {
1261+
12591262
Preconditions.notNull(clazz, "Class must not be null");
12601263
Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null");
12611264

12621265
// @formatter:off
1263-
List<Field> localFields = getDeclaredFields(clazz).stream()
1266+
List<Field> localFields = getDeclaredFields(clazz, predicate).stream()
12641267
.filter(field -> !field.isSynthetic())
12651268
.collect(toList());
1266-
List<Field> superclassFields = getSuperclassFields(clazz, traversalMode).stream()
1269+
List<Field> superclassFields = getSuperclassFields(clazz, predicate, traversalMode).stream()
12671270
.filter(field -> !isFieldShadowedByLocalFields(field, localFields))
12681271
.collect(toList());
1269-
List<Field> interfaceFields = getInterfaceFields(clazz, traversalMode).stream()
1272+
List<Field> interfaceFields = getInterfaceFields(clazz, predicate, traversalMode).stream()
12701273
.filter(field -> !isFieldShadowedByLocalFields(field, localFields))
12711274
.collect(toList());
12721275
// @formatter:on
@@ -1529,18 +1532,20 @@ private static List<Method> findAllMethodsInHierarchy(Class<?> clazz, Predicate<
15291532

15301533
/**
15311534
* Custom alternative to {@link Class#getFields()} that sorts the fields
1532-
* and converts them to a mutable list.
1535+
* which match the supplied predicate and converts them to a mutable list.
1536+
* @param predicate the field filter; never {@code null}
15331537
*/
1534-
private static List<Field> getFields(Class<?> clazz) {
1535-
return toSortedMutableList(clazz.getFields());
1538+
private static List<Field> getFields(Class<?> clazz, Predicate<Field> predicate) {
1539+
return toSortedMutableList(clazz.getFields(), predicate);
15361540
}
15371541

15381542
/**
15391543
* Custom alternative to {@link Class#getDeclaredFields()} that sorts the
1540-
* fields and converts them to a mutable list.
1544+
* fields which match the supplied predicate and converts them to a mutable list.
1545+
* @param predicate the field filter; never {@code null}
15411546
*/
1542-
private static List<Field> getDeclaredFields(Class<?> clazz) {
1543-
return toSortedMutableList(clazz.getDeclaredFields());
1547+
private static List<Field> getDeclaredFields(Class<?> clazz, Predicate<Field> predicate) {
1548+
return toSortedMutableList(clazz.getDeclaredFields(), predicate);
15441549
}
15451550

15461551
/**
@@ -1602,9 +1607,10 @@ private static List<Method> getDefaultMethods(Class<?> clazz) {
16021607
// @formatter:on
16031608
}
16041609

1605-
private static List<Field> toSortedMutableList(Field[] fields) {
1610+
private static List<Field> toSortedMutableList(Field[] fields, Predicate<Field> predicate) {
16061611
// @formatter:off
16071612
return Arrays.stream(fields)
1613+
.filter(predicate)
16081614
.sorted(ReflectionUtils::defaultFieldSorter)
16091615
// Use toCollection() instead of toList() to ensure list is mutable.
16101616
.collect(toCollection(ArrayList::new));
@@ -1672,13 +1678,15 @@ private static List<Method> getInterfaceMethods(Class<?> clazz, Predicate<Method
16721678
return allInterfaceMethods;
16731679
}
16741680

1675-
private static List<Field> getInterfaceFields(Class<?> clazz, HierarchyTraversalMode traversalMode) {
1681+
private static List<Field> getInterfaceFields(Class<?> clazz, Predicate<Field> predicate,
1682+
HierarchyTraversalMode traversalMode) {
1683+
16761684
List<Field> allInterfaceFields = new ArrayList<>();
16771685
for (Class<?> ifc : clazz.getInterfaces()) {
1678-
List<Field> localInterfaceFields = getFields(ifc);
1686+
List<Field> localInterfaceFields = getFields(ifc, predicate);
16791687

16801688
// @formatter:off
1681-
List<Field> superinterfaceFields = getInterfaceFields(ifc, traversalMode).stream()
1689+
List<Field> superinterfaceFields = getInterfaceFields(ifc, predicate, traversalMode).stream()
16821690
.filter(field -> !isFieldShadowedByLocalFields(field, localInterfaceFields))
16831691
.collect(toList());
16841692
// @formatter:on
@@ -1694,12 +1702,14 @@ private static List<Field> getInterfaceFields(Class<?> clazz, HierarchyTraversal
16941702
return allInterfaceFields;
16951703
}
16961704

1697-
private static List<Field> getSuperclassFields(Class<?> clazz, HierarchyTraversalMode traversalMode) {
1705+
private static List<Field> getSuperclassFields(Class<?> clazz, Predicate<Field> predicate,
1706+
HierarchyTraversalMode traversalMode) {
1707+
16981708
Class<?> superclass = clazz.getSuperclass();
16991709
if (!isSearchable(superclass)) {
17001710
return Collections.emptyList();
17011711
}
1702-
return findAllFieldsInHierarchy(superclass, traversalMode);
1712+
return findAllFieldsInHierarchy(superclass, predicate, traversalMode);
17031713
}
17041714

17051715
private static boolean isFieldShadowedByLocalFields(Field field, List<Field> localFields) {

platform-tests/src/test/java/org/junit/platform/commons/util/AnnotationUtilsTests.java

+26
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@
4646
import org.junit.jupiter.api.BeforeEach;
4747
import org.junit.jupiter.api.Test;
4848
import org.junit.platform.commons.PreconditionViolationException;
49+
import org.junit.platform.commons.util.pkg1.ClassLevelDir;
50+
import org.junit.platform.commons.util.pkg1.InstanceLevelDir;
4951
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateBeforeMethod;
52+
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateTempDirField;
5053
import org.junit.platform.commons.util.pkg1.subpkg.SubclassWithNonStaticPackagePrivateBeforeMethod;
54+
import org.junit.platform.commons.util.pkg1.subpkg.SubclassWithNonStaticPackagePrivateTempDirField;
5155

5256
/**
5357
* Unit tests for {@link AnnotationUtils}.
@@ -504,6 +508,28 @@ private List<Field> findShadowingAnnotatedFields(Class<? extends Annotation> ann
504508
return findAnnotatedFields(ClassWithShadowedAnnotatedFields.class, annotationType, isStringField);
505509
}
506510

511+
/**
512+
* @see https://github.com/junit-team/junit5/issues/3532
513+
*/
514+
@Test
515+
void findAnnotatedFieldsAppliesPredicateBeforeSearchingTypeHierarchy() throws Exception {
516+
final String TEMP_DIR = "tempDir";
517+
Class<?> superclass = SuperclassWithStaticPackagePrivateTempDirField.class;
518+
Field staticField = superclass.getDeclaredField(TEMP_DIR);
519+
Class<?> subclass = SubclassWithNonStaticPackagePrivateTempDirField.class;
520+
Field nonStaticField = subclass.getDeclaredField(TEMP_DIR);
521+
522+
// Prerequisite
523+
var fields = findAnnotatedFields(superclass, ClassLevelDir.class, field -> true);
524+
assertThat(fields).containsExactly(staticField);
525+
526+
// Actual use cases for this test
527+
fields = findAnnotatedFields(subclass, ClassLevelDir.class, field -> true);
528+
assertThat(fields).containsExactly(staticField);
529+
fields = findAnnotatedFields(subclass, InstanceLevelDir.class, field -> true);
530+
assertThat(fields).containsExactly(nonStaticField);
531+
}
532+
507533
// === findPublicAnnotatedFields() =========================================
508534

509535
@Test

platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java

+24
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
import org.junit.platform.commons.util.ReflectionUtilsTests.OuterClassImplementingInterface.InnerClassImplementingInterface;
7575
import org.junit.platform.commons.util.classes.CustomType;
7676
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateBeforeMethod;
77+
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateTempDirField;
7778
import org.junit.platform.commons.util.pkg1.subpkg.SubclassWithNonStaticPackagePrivateBeforeMethod;
79+
import org.junit.platform.commons.util.pkg1.subpkg.SubclassWithNonStaticPackagePrivateTempDirField;
7880

7981
/**
8082
* Unit tests for {@link ReflectionUtils}.
@@ -1379,6 +1381,28 @@ void isGeneric() {
13791381
}
13801382
}
13811383

1384+
/**
1385+
* @see https://github.com/junit-team/junit5/issues/3532
1386+
*/
1387+
@Test
1388+
void findFieldsAppliesPredicateBeforeSearchingTypeHierarchy() throws Exception {
1389+
final String TEMP_DIR = "tempDir";
1390+
Class<?> superclass = SuperclassWithStaticPackagePrivateTempDirField.class;
1391+
Field staticField = superclass.getDeclaredField(TEMP_DIR);
1392+
Class<?> subclass = SubclassWithNonStaticPackagePrivateTempDirField.class;
1393+
Field nonStaticField = subclass.getDeclaredField(TEMP_DIR);
1394+
1395+
// Prerequisite
1396+
var fields = findFields(superclass, ReflectionUtils::isStatic, TOP_DOWN);
1397+
assertThat(fields).containsExactly(staticField);
1398+
1399+
// Actual use cases for this test
1400+
fields = findFields(subclass, ReflectionUtils::isStatic, TOP_DOWN);
1401+
assertThat(fields).containsExactly(staticField);
1402+
fields = findFields(subclass, ReflectionUtils::isNotStatic, TOP_DOWN);
1403+
assertThat(fields).containsExactly(nonStaticField);
1404+
}
1405+
13821406
@Test
13831407
void readFieldValuesPreconditions() {
13841408
assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.readFieldValues(null, new Object()));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2015-2023 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.commons.util.pkg1;
12+
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
18+
/**
19+
* Mimics {@code @TempDir}.
20+
*/
21+
@Target(ElementType.FIELD)
22+
@Retention(RetentionPolicy.RUNTIME)
23+
public @interface ClassLevelDir {
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2015-2023 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.commons.util.pkg1;
12+
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
18+
/**
19+
* Mimics {@code @TempDir}.
20+
*/
21+
@Target(ElementType.FIELD)
22+
@Retention(RetentionPolicy.RUNTIME)
23+
public @interface InstanceLevelDir {
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2015-2023 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.commons.util.pkg1;
12+
13+
import java.nio.file.Path;
14+
15+
/**
16+
* @see https://github.com/junit-team/junit5/issues/3532
17+
*/
18+
public class SuperclassWithStaticPackagePrivateTempDirField {
19+
20+
@ClassLevelDir
21+
static Path tempDir;
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2015-2023 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.commons.util.pkg1.subpkg;
12+
13+
import java.nio.file.Path;
14+
15+
import org.junit.platform.commons.util.pkg1.InstanceLevelDir;
16+
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateTempDirField;
17+
18+
/**
19+
* @see https://github.com/junit-team/junit5/issues/3532
20+
*/
21+
public class SubclassWithNonStaticPackagePrivateTempDirField extends SuperclassWithStaticPackagePrivateTempDirField {
22+
23+
@InstanceLevelDir
24+
Path tempDir;
25+
26+
}

0 commit comments

Comments
 (0)