Skip to content

Commit 4e228a5

Browse files
committed
Fix StackOverflowError in TypeUtils.typeVariableToString(TypeVariable),
TypeUtils.toString(Type) - https://issues.apache.org/jira/projects/LANG/issues/LANG-1698 - Happens on Java 17 and up - Tested locally on Java 8, 17, 21, and 23
1 parent 6681a34 commit 4e228a5

File tree

3 files changed

+45
-12
lines changed

3 files changed

+45
-12
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ The <action> type attribute can be add,update,fix,remove.
6363
<action issue="LANG-1682" type="fix" dev="ggregory" due-to="Capt. Cutlass">Javadoc and test: Use Strings.CI.startsWithAny method instead #1299.</action>
6464
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix NullPointerException in FastDateParser.TimeZoneStrategy.setCalendar(FastDateParser, Calendar, String) on Java 23.</action>
6565
<action issue="LANG-1757" type="fix" dev="ggregory" due-to="Gary Gregory">Fix NullPointerException in MethodUtils.getMatchingAccessibleMethod((Class, String, Class...)).</action>
66+
<action issue="LANG-1698" type="fix" dev="ggregory" due-to="Jan Arne Sparka, Gary Gregory">Fix StackOverflowError in TypeUtils.typeVariableToString(TypeVariable), TypeUtils.toString(Type) on Java 17 and up.</action>
6667
<!-- ADD -->
6768
<action type="add" dev="ggregory" due-to="Gary Gregory">Add Strings and refactor StringUtils.</action>
6869
<action issue="LANG-1747" type="add" dev="ggregory" due-to="Oliver B. Fischer, Gary Gregory">Add StopWatch.run([Failable]Runnable) and get([Failable]Supplier).</action>

src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public String toString() {
291291
// @formatter:off
292292
private static final AppendableJoiner<Type> AMP_JOINER = AppendableJoiner.<Type>builder()
293293
.setDelimiter(" & ")
294-
.setElementAppender((a, e) -> a.append(TypeUtils.toString(e)))
294+
.setElementAppender((a, e) -> a.append(toString(e)))
295295
.get();
296296
// @formatter:on
297297

@@ -301,7 +301,7 @@ public String toString() {
301301
// @formatter:off
302302
private static final AppendableJoiner<TypeVariable<Class<?>>> CTJ_JOINER = AppendableJoiner.<TypeVariable<Class<?>>>builder()
303303
.setDelimiter(", ")
304-
.setElementAppender((a, e) -> a.append(TypeUtils.anyToString(e)))
304+
.setElementAppender((a, e) -> a.append(anyToString(e)))
305305
.get();
306306
// @formatter:on
307307

@@ -313,7 +313,7 @@ public String toString() {
313313
.setPrefix("<")
314314
.setSuffix(">")
315315
.setDelimiter(", ")
316-
.setElementAppender((a, e) -> a.append(TypeUtils.anyToString(e)))
316+
.setElementAppender((a, e) -> a.append(anyToString(e)))
317317
.get();
318318
// @formatter:on
319319

@@ -359,7 +359,6 @@ private static <T> String classToString(final Class<T> cls) {
359359
buf.append(cls.getName());
360360
}
361361
if (cls.getTypeParameters().length > 0) {
362-
// AppendableJoiner.joinSB(buf, null, null, ", ", TypeUtils::anyToString, cls.getTypeParameters());
363362
CTJ_JOINER.join(buf, (TypeVariable[]) cls.getTypeParameters());
364363
}
365364
return buf.toString();
@@ -1535,7 +1534,7 @@ private static String parameterizedTypeToString(final ParameterizedType paramete
15351534
if (recursiveTypeIndexes.length > 0) {
15361535
appendRecursiveTypes(builder, recursiveTypeIndexes, parameterizedType.getActualTypeArguments());
15371536
} else {
1538-
GT_JOINER.join(builder, parameterizedType.getActualTypeArguments());
1537+
GT_JOINER.join(builder, (Object[]) parameterizedType.getActualTypeArguments());
15391538
}
15401539
return builder.toString();
15411540
}
@@ -1700,8 +1699,22 @@ private static String typeVariableToString(final TypeVariable<?> typeVariable) {
17001699
final StringBuilder builder = new StringBuilder(typeVariable.getName());
17011700
final Type[] bounds = typeVariable.getBounds();
17021701
if (bounds.length > 0 && !(bounds.length == 1 && Object.class.equals(bounds[0]))) {
1703-
builder.append(" extends ");
1704-
AMP_JOINER.join(builder, typeVariable.getBounds());
1702+
// https://issues.apache.org/jira/projects/LANG/issues/LANG-1698
1703+
// There must be a better way to avoid a stack overflow on Java 17 and up.
1704+
// Bounds are different in Java 17 and up where instead of Object you can get an interface like Comparable.
1705+
final Type bound = bounds[0];
1706+
boolean append = true;
1707+
if (bound instanceof ParameterizedType) {
1708+
final Type rawType = ((ParameterizedType) bound).getRawType();
1709+
if (rawType instanceof Class && ((Class<?>) rawType).isInterface()) {
1710+
// Avoid recursion and stack overflow on Java 17 and up.
1711+
append = false;
1712+
}
1713+
}
1714+
if (append) {
1715+
builder.append(" extends ");
1716+
AMP_JOINER.join(builder, bounds);
1717+
}
17051718
}
17061719
return builder.toString();
17071720
}

src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2020
import static org.junit.jupiter.api.Assertions.assertEquals;
2121
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertNotNull;
2223
import static org.junit.jupiter.api.Assertions.assertNull;
2324
import static org.junit.jupiter.api.Assertions.assertThrows;
2425
import static org.junit.jupiter.api.Assertions.assertTrue;
25-
import static org.junit.jupiter.api.Assumptions.assumeTrue;
2626

2727
import java.awt.Insets;
2828
import java.io.Serializable;
@@ -45,16 +45,17 @@
4545
import java.util.Map;
4646
import java.util.Properties;
4747
import java.util.TreeSet;
48+
import java.util.stream.Stream;
4849

4950
import org.apache.commons.lang3.AbstractLangTest;
50-
import org.apache.commons.lang3.JavaVersion;
51-
import org.apache.commons.lang3.SystemUtils;
5251
import org.apache.commons.lang3.reflect.testbed.Foo;
5352
import org.apache.commons.lang3.reflect.testbed.GenericParent;
5453
import org.apache.commons.lang3.reflect.testbed.GenericTypeHolder;
5554
import org.apache.commons.lang3.reflect.testbed.StringParameterizedChild;
5655
import org.junit.jupiter.api.Disabled;
5756
import org.junit.jupiter.api.Test;
57+
import org.junit.jupiter.params.ParameterizedTest;
58+
import org.junit.jupiter.params.provider.MethodSource;
5859

5960
/**
6061
* Test fixture for https://issues.apache.org/jira/browse/LANG-1524
@@ -243,6 +244,17 @@ public static <T extends Comparable<? extends T>> T stub3() {
243244
return null;
244245
}
245246

247+
static Stream<Type> testTypeToString() {
248+
// @formatter:off
249+
return Stream.of(Comparator.class, Comparable.class, ArrayList.class, HashMap.class)
250+
.flatMap(cls -> Stream.of(cls.getDeclaredMethods()))
251+
.flatMap(m ->
252+
Stream.concat(Stream.of(m.getGenericExceptionTypes()),
253+
Stream.concat(Stream.of(m.getGenericParameterTypes()),
254+
Stream.concat(Stream.of(m.getGenericReturnType()), Stream.of(m.getTypeParameters())))));
255+
// @formatter:on
256+
}
257+
246258
public The<String, String> da;
247259

248260
public That<String, String> dat;
@@ -301,6 +313,8 @@ public void test_LANG_1524() {
301313
}
302314

303315
/**
316+
* Tests https://issues.apache.org/jira/projects/LANG/issues/LANG-1698
317+
*
304318
* <pre>{@code
305319
* java.lang.StackOverflowError
306320
at org.apache.commons.lang3.reflect.TypeUtils.typeVariableToString(TypeUtils.java:1785)
@@ -321,8 +335,6 @@ public void test_LANG_1524() {
321335
*/
322336
@Test
323337
public void test_LANG_1698() {
324-
// SO on Java 17
325-
assumeTrue(SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_16));
326338
final ParameterizedType comparing = (ParameterizedType) Arrays.stream(Comparator.class.getDeclaredMethods())
327339
.filter(k -> k.getName().equals("comparing")).findFirst()
328340
.orElse(Comparator.class.getDeclaredMethods()[0]).getGenericParameterTypes()[0];
@@ -1024,6 +1036,13 @@ public void testTypesSatisfyVariables() throws NoSuchMethodException {
10241036
assertThrows(NullPointerException.class, () -> TypeUtils.typesSatisfyVariables(null));
10251037
}
10261038

1039+
@ParameterizedTest
1040+
@MethodSource
1041+
public void testTypeToString(Type type) {
1042+
// No stack overflow
1043+
assertNotNull(TypeUtils.toString(type));
1044+
}
1045+
10271046
@Test
10281047
public void testUnboundedWildcardType() {
10291048
final WildcardType unbounded = TypeUtils.wildcardType().withLowerBounds((Type) null).withUpperBounds().build();

0 commit comments

Comments
 (0)