You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/05/01 05:14:49 UTC
(groovy) branch GROOVY_5_0_X updated: GROOVY-11022: StackOverflowError when having parameterized function with recursive bounds
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch GROOVY_5_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY_5_0_X by this push:
new 9f5fb29d13 GROOVY-11022: StackOverflowError when having parameterized function with recursive bounds
9f5fb29d13 is described below
commit 9f5fb29d13ca521878f3f9cb97d7b653c1caae1a
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Apr 28 15:27:08 2026 +1000
GROOVY-11022: StackOverflowError when having parameterized function with recursive bounds
---
.../transform/stc/StaticTypeCheckingSupport.java | 82 ++++++++++++++++++----
.../groovy/transform/stc/GenericsSTCTest.groovy | 36 ++++++++++
2 files changed, 105 insertions(+), 13 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
index ea9de4082b..961a7adc49 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
@@ -55,11 +55,13 @@ import org.codehaus.groovy.tools.GroovyClass;
import org.codehaus.groovy.transform.trait.Traits;
import org.objectweb.asm.Opcodes;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -1841,19 +1843,52 @@ public abstract class StaticTypeCheckingSupport {
return type.getGenericsTypes();
}
+ /**
+ * Tracks placeholder names whose bounds are currently being expanded by
+ * {@link #applyGenericsContext}, so the recursion can break cycles caused
+ * by F-bounded type parameters whose self-reference appears inside a
+ * wildcard bound (GROOVY-11022). The backing deque is allocated lazily —
+ * the cycle-breaking machinery is only needed when an F-bounded
+ * placeholder is actually encountered, which is rare on the STC hot path.
+ */
+ private static final class BoundExpansionContext {
+ private Deque<GenericsTypeName> expanding;
+
+ boolean isExpanding(final GenericsTypeName name) {
+ return expanding != null && expanding.contains(name);
+ }
+
+ void enter(final GenericsTypeName name) {
+ if (expanding == null) expanding = new ArrayDeque<>();
+ expanding.push(name);
+ }
+
+ void exit() {
+ expanding.pop();
+ }
+ }
+
static GenericsType[] applyGenericsContext(final Map<GenericsTypeName, GenericsType> spec, final GenericsType[] gts) {
if (gts == null || spec == null || spec.isEmpty()) return gts;
+ return applyGenericsContext(new BoundExpansionContext(), spec, gts);
+ }
+
+ private static GenericsType applyGenericsContext(final Map<GenericsTypeName, GenericsType> spec, final GenericsType gt) {
+ return applyGenericsContext(new BoundExpansionContext(), spec, gt);
+ }
+ private static GenericsType[] applyGenericsContext(final BoundExpansionContext ctx, final Map<GenericsTypeName, GenericsType> spec, final GenericsType[] gts) {
+ if (gts == null) return null;
int n = gts.length;
if (n == 0) return gts;
GenericsType[] newGTs = new GenericsType[n];
for (int i = 0; i < n; i += 1) {
- newGTs[i] = applyGenericsContext(spec, gts[i]);
+ newGTs[i] = applyGenericsContext(ctx, spec, gts[i]);
}
return newGTs;
}
- private static GenericsType applyGenericsContext(final Map<GenericsTypeName, GenericsType> spec, final GenericsType gt) {
+ private static GenericsType applyGenericsContext(final BoundExpansionContext ctx, final Map<GenericsTypeName, GenericsType> spec, final GenericsType gt) {
ClassNode type = gt.getType();
if (gt.isPlaceholder()) {
@@ -1861,16 +1896,25 @@ public abstract class StaticTypeCheckingSupport {
GenericsType specType = spec.get(name);
if (specType != null) return specType;
if (hasNonTrivialBounds(gt)) {
- GenericsType newGT = new GenericsType(type, applyGenericsContext(spec, gt.getUpperBounds()), applyGenericsContext(spec, gt.getLowerBound()));
- newGT.setPlaceholder(true);
- return newGT;
+ // GROOVY-11022: avoid infinite recursion on F-bounded type
+ // parameters whose self-reference appears inside a wildcard
+ // bound (e.g. `<K extends Comparable<? super K>>`)
+ if (ctx.isExpanding(name)) return gt;
+ ctx.enter(name);
+ try {
+ GenericsType newGT = new GenericsType(type, applyGenericsContext(ctx, spec, gt.getUpperBounds()), applyGenericsContext(ctx, spec, gt.getLowerBound()));
+ newGT.setPlaceholder(true);
+ return newGT;
+ } finally {
+ ctx.exit();
+ }
}
return gt;
}
if (gt.isWildcard()) { // TODO: What if a bound itself resolves to a wildcard?
- ClassNode[] upperBounds = applyGenericsContext(spec, gt.getUpperBounds());
- ClassNode lowerBound = applyGenericsContext(spec, gt.getLowerBound());
+ ClassNode[] upperBounds = applyGenericsContext(ctx, spec, gt.getUpperBounds());
+ ClassNode lowerBound = applyGenericsContext(ctx, spec, gt.getLowerBound());
GenericsType newGT = new GenericsType(type, upperBounds, lowerBound);
newGT.setWildcard(true);
return newGT;
@@ -1878,20 +1922,20 @@ public abstract class StaticTypeCheckingSupport {
ClassNode newType;
if (type.isArray()) {
- newType = applyGenericsContext(spec, type.getComponentType()).makeArray();
+ newType = applyGenericsContext(ctx, spec, type.getComponentType()).makeArray();
} else if (type.getGenericsTypes() == null//type.isGenericsPlaceHolder()
&& type.getOuterClass() == null) {
return gt;
} else {
newType = type.getPlainNodeReference();
newType.setGenericsPlaceHolder(type.isGenericsPlaceHolder());
- newType.setGenericsTypes(applyGenericsContext(spec, type.getGenericsTypes()));
+ newType.setGenericsTypes(applyGenericsContext(ctx, spec, type.getGenericsTypes()));
// GROOVY-10646: non-static inner class + outer class type parameter
if ((type.getModifiers() & Opcodes.ACC_STATIC) == 0) {
Optional.ofNullable(type.getOuterClass())
.filter(oc -> oc.getGenericsTypes()!=null)
- .map(oc -> applyGenericsContext(spec, oc))
+ .map(oc -> applyGenericsContext(ctx, spec, oc))
.ifPresent(oc -> newType.putNodeMetaData("outer.class", oc));
}
}
@@ -1913,26 +1957,38 @@ public abstract class StaticTypeCheckingSupport {
}
static ClassNode[] applyGenericsContext(final Map<GenericsTypeName, GenericsType> spec, final ClassNode[] types) {
+ if (types == null) return null;
+ return applyGenericsContext(new BoundExpansionContext(), spec, types);
+ }
+
+ private static ClassNode[] applyGenericsContext(final BoundExpansionContext ctx, final Map<GenericsTypeName, GenericsType> spec, final ClassNode[] types) {
if (types == null) return null;
final int nTypes = types.length;
ClassNode[] newTypes = new ClassNode[nTypes];
for (int i = 0; i < nTypes; i += 1) {
- newTypes[i] = applyGenericsContext(spec, types[i]);
+ newTypes[i] = applyGenericsContext(ctx, spec, types[i]);
}
return newTypes;
}
static ClassNode applyGenericsContext(final Map<GenericsTypeName, GenericsType> spec, final ClassNode type) {
+ if (type == null || !isUsingGenericsOrIsArrayUsingGenerics(type)) {
+ return type;
+ }
+ return applyGenericsContext(new BoundExpansionContext(), spec, type);
+ }
+
+ private static ClassNode applyGenericsContext(final BoundExpansionContext ctx, final Map<GenericsTypeName, GenericsType> spec, final ClassNode type) {
if (type == null || !isUsingGenericsOrIsArrayUsingGenerics(type)) {
return type;
}
if (type.isArray()) {
- return applyGenericsContext(spec, type.getComponentType()).makeArray();
+ return applyGenericsContext(ctx, spec, type.getComponentType()).makeArray();
}
GenericsType[] gt = type.getGenericsTypes();
if (asBoolean(spec)) {
- gt = applyGenericsContext(spec, gt);
+ gt = applyGenericsContext(ctx, spec, gt);
}
if (!type.isGenericsPlaceHolder()) { // convert Type<T> to Type<...>
ClassNode cn = type.getPlainNodeReference();
diff --git a/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
index 7d566a03a0..982aed8010 100644
--- a/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/GenericsSTCTest.groovy
@@ -4368,6 +4368,42 @@ class GenericsSTCTest extends StaticTypeCheckingTestCase {
}
}
+ // GROOVY-11022
+ void testNoStackOverflow3() {
+ // F-bounded type parameter with self-reference inside a `? super` wildcard
+ assertScript '''
+ class C<K, V> {
+ public static <K extends Comparable<? super K>, V> C<K, V> m(K k, V v) {
+ new C<K, V>()
+ }
+ }
+ class Main {
+ @groovy.transform.TypeChecked
+ static test() {
+ C.<Integer, Integer>m(null, 1)
+ }
+ }
+ Main.test()
+ '''
+
+ // self-reference inside a `? extends` wildcard
+ assertScript '''
+ class Self implements Iterable<Self> {
+ Iterator<Self> iterator() { Collections.emptyIterator() }
+ }
+ class C<K> {
+ public static <K extends Iterable<? extends K>> C<K> m(K k) {
+ new C<K>()
+ }
+ }
+ @groovy.transform.TypeChecked
+ void test() {
+ C.<Self>m(null)
+ }
+ test()
+ '''
+ }
+
void testRegressionInConstructorCheck() {
assertScript '''
new ArrayList(['a','b','c'].collect { String it -> it.toUpperCase()})