Skip to content

Fix TypeUtils.toString() recursion and bound handling on recursive generic types - #1789

Merged
garydgregory merged 3 commits into
apache:masterfrom
Alwaysgaurav1:fix/typeutils-tostring-recursive-bounds
Sep 13, 2026
Merged

garydgregory merged 3 commits into
apache:masterfrom
Alwaysgaurav1:fix/typeutils-tostring-recursive-bounds

Conversation

@Alwaysgaurav1

Copy link
Copy Markdown
Contributor

Problem

TypeUtils.toString(Type) suffered from several interrelated issues when handling recursive generic types and type bounds:

  1. StackOverflowError on Recursive Generic Types: Formatting recursive generic types like class MySuperClass<T>, class MyClass<U extends MySuperClass<? super U>>, or class MultiBoundClass<U extends Number & Comparable<? super U>> resulted in StackOverflowError.
  2. Stripping of Valid Interface Bounds: A workaround previously introduced for LANG-1698 unconditionally skipped bounds if the raw type was an interface. This stripped all valid interface bounds (e.g. <T extends List<String>> formatted as "T" instead of "T extends java.util.List<java.lang.String>"), while still failing with a StackOverflowError on class bounds.
  3. Corrupted Multi-Parameter Recursive Types: Workaround methods findRecursiveTypes and appendRecursiveTypes corrupted type parameter formatting on multi-parameter recursive types (such as TwoParams<T extends TwoParams<T, U>, U>), outputting <T><U><U>.
  4. Incomplete Multi-Bound Wildcard Inspection: TypeUtils.containsTypeVariables(WildcardType) only inspected the bound at index 0, ignoring subsequent bounds for multi-bounded wildcards.

Root Cause

In Java grammar (JLS Chapter 4), a TypeVariable is a declaration only when declaring a type parameter (e.g. <U extends Number>). Everywhere else—such as inside type arguments (List<U>), wildcard bounds (? extends U, ? super U), generic arrays (U[]), or other type variable bounds (<S extends T>)—it is a type reference (ReferenceType) and should be formatted only by its identifier name U. Treating embedded type variables as declarations led to recursive expansion of bounds.

Solution

  1. Differentiate Type Declarations and References: Added toReferenceString(Type) which formats TypeVariable instances by name only and delegates other types to toString(Type). Configured AMP_JOINER, TYPE_ARG_JOINER, and genericArrayTypeToString to format embedded types as references.
  2. Preserve Interface Bounds: Removed the interface-check workaround from typeVariableToString, properly preserving all interface bounds without recursion.
  3. Remove Workaround Heuristics: Cleanly removed findRecursiveTypes, appendRecursiveTypes, and containsVariableTypeSameParametrizedTypeBound.
  4. Inspect All Wildcard Bounds: Updated containsTypeVariables(WildcardType) to check all implicit upper and lower bounds.
  5. ThreadLocal Cycle Guard: Added VISITING ThreadLocal<Set<TypeVariable<?>>> with guaranteed VISITING.remove() in finally to guard against arbitrary cyclical type graphs without leaking memory.

Verification

  • All 430 tests in TypeUtilsTest passed (including 6 new tests covering recursive class bounds, multi-bounds, interface preservation, and multi-bound wildcards).
  • All 590 tests across org.apache.commons.lang3.reflect.* passed without regressions.

…neric types

- Format embedded TypeVariables as type references (name only) in ParameterizedType type arguments, WildcardType bounds, and GenericArrayType component types, preventing recursion and StackOverflowError
- Preserve valid interface bounds on TypeVariables instead of stripping them
- Remove fragile heuristic methods findRecursiveTypes, appendRecursiveTypes, and containsVariableTypeSameParametrizedTypeBound that corrupted formatting to <T><U><U>
- Add recursion guard with VISITING ThreadLocal cleanup on unwind
- Fix TypeUtils.containsTypeVariables(WildcardType) to check all upper and lower bounds instead of only index 0
@Alwaysgaurav1

Copy link
Copy Markdown
Contributor Author

Hi @garydgregory, could you please review..

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The recursion guard does not cover all cyclic Type graphs, leaving a critical StackOverflowError path.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This pull request fixes recursive generic type formatting and preserves valid bounds in TypeUtils.

Changes:

  • Separates type declarations from references.
  • Preserves interface and multi-bounded wildcard bounds.
  • Replaces recursive-formatting workarounds with cycle protection.
  • Adds regression coverage.
File summaries
File Summary
src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java Adds tests for recursive, multi-bound, interface, and wildcard cases.
src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java Updates type formatting, bound handling, wildcard inspection, and recursion protection.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No unresolved blocking issues were identified.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @Alwaysgaurav1
Thank you for the update.
I'm not sure the new tests do not cover every change:

  • Bounded T[], List, and <T extends Number, S extends T> formatting.
  • A bounded variable passed to toLongString().
  • A parameterized owner with a non-generic inner class, exercising removal of <>.
  • Defensive copying of lower bounds; only upper bounds are tested.
  • Owner cycles, repeated sibling references, and cleanup after an exception.

- Add tests for bounded T[], List<T>, and <T extends Number, S extends T> formatting
- Add tests for bounded TypeVariable passed to toLongString()
- Add test for parameterized owner with non-generic inner class exercising removal of <>
- Add test for defensive copying of lower bounds in WildcardTypeImpl
- Add tests for owner cycles, repeated sibling references, and ThreadLocal cleanup after exception
- Route non-Class owner types and toLongString TypeVariables through toString(Type) for cycle tracking
@Alwaysgaurav1

Copy link
Copy Markdown
Contributor Author

Hello @garydgregory,

Thank you for the thorough review! I have addressed all five points in the latest push (commit df5c62c):

  1. Bounded T[], List<T>, and <T extends Number, S extends T> formatting: Added dedicated tests (testBoundedGenericArrayTypeToString, testBoundedParameterizedTypeArgumentToString, and testDependentBoundsClassAndTypeParametersToString) verifying that type variables in arrays, type arguments, and dependent type bounds format as type references without expanding recursively.
  2. Bounded variable passed to toLongString(): Updated toLongString to route through toString(typeVariable) for cycle safety, and added testToLongStringBoundedTypeVariable covering single bounds, dependent bounds, multi-bounds, interface bounds, and recursive class bounds.
  3. Parameterized owner with a non-generic inner class: Added testParameterizedOwnerWithNonGenericInnerClassToString exercising removal of <> for both parameterizeWithOwner(...) and reflective method return types.
  4. Defensive copying of lower bounds: Updated testWildcardTypeBuilderDefensiveCopy to test input array and getter return defensive copying for both upper and lower bounds.
  5. Owner cycles, repeated sibling references, and exception cleanup:
    • Routed non-Class owner formatting through toString(useOwner) so owner cycles are guarded, adding testCyclicOwnerParameterizedTypeToString (self-cycles and mutual owner cycles).
    • Added testRepeatedSiblingReferencesToString verifying that identical sibling type instances in a graph are not falsely flagged as cycles.
    • Added testThreadLocalCleanupAfterException verifying clean unwind of the ThreadLocal guard after exceptions (unsupported types, custom exceptions, nested exceptions) and correct subsequent behavior.

All 442 tests in TypeUtilsTest pass, and Checkstyle reports 0 violations.

@garydgregory
garydgregory merged commit e1ce120 into apache:master Sep 13, 2026
23 of 24 checks passed
garydgregory added a commit that referenced this pull request Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants