@@ -207,6 +207,10 @@ public static ClassNode lowestUpperBound(final List<ClassNode> nodes) {
207207 * @since 2.0.0
208208 */
209209 public static ClassNode lowestUpperBound (final ClassNode a , final ClassNode b ) {
210+ return lowestUpperBound (new LowestUpperBoundContext (), a , b );
211+ }
212+
213+ private static ClassNode lowestUpperBound (final LowestUpperBoundContext ctx , final ClassNode a , final ClassNode b ) {
210214 ClassNode lub = lowestUpperBound (a , b , null , null );
211215 if (lub == null || !lub .isUsingGenerics ()
212216 || lub .isGenericsPlaceHolder ()) { // GROOVY-10330
@@ -222,20 +226,20 @@ public static ClassNode lowestUpperBound(final ClassNode a, final ClassNode b) {
222226 // plus the interfaces
223227 ClassNode superClass = lub .getSuperClass ();
224228 if (superClass .redirect ().getGenericsTypes () != null ) {
225- superClass = parameterizeLowestUpperBound (superClass , a , b , lub );
229+ superClass = parameterizeLowestUpperBound (ctx , superClass , a , b , lub );
226230 }
227231
228232 ClassNode [] interfaces = lub .getInterfaces ().clone ();
229233 for (int i = 0 , n = interfaces .length ; i < n ; i += 1 ) {
230234 ClassNode icn = interfaces [i ];
231235 if (icn .redirect ().getGenericsTypes () != null ) {
232- interfaces [i ] = parameterizeLowestUpperBound (icn , a , b , lub );
236+ interfaces [i ] = parameterizeLowestUpperBound (ctx , icn , a , b , lub );
233237 }
234238 }
235239
236240 return new LowestUpperBoundClassNode (lub .getUnresolvedName (), superClass , interfaces );
237241 } else {
238- return parameterizeLowestUpperBound (lub , a , b , lub );
242+ return parameterizeLowestUpperBound (ctx , lub , a , b , lub );
239243 }
240244 }
241245
@@ -246,13 +250,14 @@ public static ClassNode lowestUpperBound(final ClassNode a, final ClassNode b) {
246250 *
247251 * For example, if LUB is Set<T> and a is Set<String> and b is Set<StringBuffer>, this
248252 * will return a LUB which parameterized type matches Set<? extends CharSequence>
253+ * @param ctx tracks (t1, t2) pairs whose LUB is currently being computed, so this method can detect recursive calls (GROOVY-11770)
249254 * @param lub the type to be parameterized
250255 * @param a parameterized type a
251256 * @param b parameterized type b
252257 * @param fallback if we detect a recursive call, use this LUB as the parameterized type instead of computing a value
253258 * @return the class node representing the parameterized lowest upper bound
254259 */
255- private static ClassNode parameterizeLowestUpperBound (final ClassNode lub , final ClassNode a , final ClassNode b , final ClassNode fallback ) {
260+ private static ClassNode parameterizeLowestUpperBound (final LowestUpperBoundContext ctx , final ClassNode lub , final ClassNode a , final ClassNode b , final ClassNode fallback ) {
256261 if (a .toString (false ).equals (b .toString (false ))) return lub ;
257262 // a common super type exists, all we have to do is to parameterize
258263 // it according to the types provided by the two class nodes
@@ -273,11 +278,16 @@ private static ClassNode parameterizeLowestUpperBound(final ClassNode lub, final
273278 if (areEqualWithGenerics (t1 , isPrimitiveType (a )?getWrapper (a ):a ) && areEqualWithGenerics (t2 , isPrimitiveType (b )?getWrapper (b ):b )) {
274279 // "String implements Comparable<String>" and "StringBuffer implements Comparable<StringBuffer>"
275280 basicType = fallback ; // do not loop
281+ } else if (ctx .isExpanding (t1 , t2 )) {
282+ // GROOVY-11770: recursion guard for an already-expanding type pair, or depth cap reached
283+ // (e.g. LUB(B, D) where B extends A<W<B>>, D extends A<W<D>>)
284+ basicType = fallback ;
276285 } else {
286+ ctx .enter (t1 , t2 );
277287 try {
278- basicType = lowestUpperBound (t1 , t2 );
279- } catch ( StackOverflowError ignore ) {
280- basicType = fallback ; // best we can do for now
288+ basicType = lowestUpperBound (ctx , t1 , t2 );
289+ } finally {
290+ ctx . exit ( t1 , t2 );
281291 }
282292 }
283293 if (agt [i ].isWildcard () || bgt [i ].isWildcard () || !t1 .equals (t2 )) {
@@ -289,6 +299,57 @@ private static ClassNode parameterizeLowestUpperBound(final ClassNode lub, final
289299 return GenericsUtils .makeClassSafe0 (lub , lubGTs );
290300 }
291301
302+ /**
303+ * Tracks pairs of types whose LUB is currently being computed by
304+ * {@link #lowestUpperBound(ClassNode, ClassNode)}, so the recursion can
305+ * break cycles caused by F-bounded type parameters that route a subtype
306+ * back through itself (GROOVY-11770). Pair equality is identity-based and
307+ * order-insensitive, since LUB is symmetric. A depth cap acts as a
308+ * backstop in case generics rewriting yields fresh {@code ClassNode}
309+ * instances for logically identical types, which would defeat identity
310+ * tracking.
311+ */
312+ private static final class LowestUpperBoundContext {
313+ private static final int MAX_DEPTH = 64 ;
314+ private final Set <TypePair > inflight = new HashSet <>();
315+ private int depth ;
316+
317+ boolean isExpanding (final ClassNode a , final ClassNode b ) {
318+ return depth >= MAX_DEPTH || inflight .contains (new TypePair (a , b ));
319+ }
320+
321+ void enter (final ClassNode a , final ClassNode b ) {
322+ inflight .add (new TypePair (a , b ));
323+ depth ++;
324+ }
325+
326+ void exit (final ClassNode a , final ClassNode b ) {
327+ inflight .remove (new TypePair (a , b ));
328+ depth --;
329+ }
330+
331+ private static final class TypePair {
332+ final ClassNode a , b ;
333+
334+ TypePair (final ClassNode a , final ClassNode b ) {
335+ this .a = a ;
336+ this .b = b ;
337+ }
338+
339+ @ Override
340+ public boolean equals (final Object o ) {
341+ if (!(o instanceof TypePair )) return false ;
342+ TypePair p = (TypePair ) o ;
343+ return (a == p .a && b == p .b ) || (a == p .b && b == p .a );
344+ }
345+
346+ @ Override
347+ public int hashCode () {
348+ return System .identityHashCode (a ) ^ System .identityHashCode (b );
349+ }
350+ }
351+ }
352+
292353 private static ClassNode findGenericsTypeHolderForClass (ClassNode source , final ClassNode target ) {
293354 if (isPrimitiveType (source )) source = getWrapper (source );
294355 if (source .equals (target )) {
0 commit comments