This is an automated email from the ASF dual-hosted git repository. paulk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 3a4978d09ff7d3c79e918f94dc523e5c3b43a929 Author: Paul King <[email protected]> AuthorDate: Mon Mar 30 11:09:48 2026 +1000 GROOVY-11888: STC: method resolution fails for UnionTypeClassNode due to premature covariant elimination --- .../transform/stc/StaticTypeCheckingSupport.java | 6 +- .../transform/stc/TypeInferenceSTCTest.groovy | 138 +++++++++++++++++++++ 2 files changed, 142 insertions(+), 2 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 cb84c11b1c..c7676a2ba4 100644 --- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java +++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java @@ -953,8 +953,8 @@ public abstract class StaticTypeCheckingSupport { // GROOVY-8965: type disjunction boolean duckType = receiver instanceof UnionTypeClassNode; - if (methods.size() > 1 && !first(methods).isConstructor()) - methods = removeCovariantsAndInterfaceEquivalents(methods, duckType); + if (!duckType && methods.size() > 1 && !first(methods).isConstructor()) + methods = removeCovariantsAndInterfaceEquivalents(methods, false); if (!duckType && argumentTypes == null) { return asList(methods); // GROOVY-11683: no covariants or equivalents @@ -965,6 +965,8 @@ public abstract class StaticTypeCheckingSupport { var view = methods; if (duckType) { view = methods.stream().filter(m -> implementsInterfaceOrSubclassOf(rcvr, m.getDeclaringClass())).toList(); + if (view.size() > 1 && !first(view).isConstructor()) + view = removeCovariantsAndInterfaceEquivalents(view, true); } view = chooseBestMethods(rcvr, view, argumentTypes); if (view.isEmpty()) { diff --git a/src/test/groovy/groovy/transform/stc/TypeInferenceSTCTest.groovy b/src/test/groovy/groovy/transform/stc/TypeInferenceSTCTest.groovy index c6a9a17c1b..4adea4b051 100644 --- a/src/test/groovy/groovy/transform/stc/TypeInferenceSTCTest.groovy +++ b/src/test/groovy/groovy/transform/stc/TypeInferenceSTCTest.groovy @@ -431,6 +431,144 @@ class TypeInferenceSTCTest extends StaticTypeCheckingTestCase { 'Incompatible instanceof types: java.lang.Integer and java.lang.Long' } + // GROOVY-7971: nested && within || — method calls on narrowed types within + // each && branch should work; the || produces a union for the body + @Test + void testInstanceOf18() { + assertScript ''' + int test(Object x) { + if (x instanceof String && x.length() > 0 || x instanceof List && x.size() > 0) { + return 1 + } + return 0 + } + assert test('hello') == 1 + assert test([1, 2, 3]) == 1 + assert test('') == 0 + assert test([]) == 0 + assert test(42) == 0 + ''' + } + + // GROOVY-7971: ternary with || instanceof in condition + @Test + void testInstanceOf19() { + assertScript ''' + String test(Object x) { + (x instanceof String || x instanceof Integer) ? x.toString() : 'other' + } + assert test('hi') == 'hi' + assert test(42) == '42' + assert test(3.14) == 'other' + ''' + } + + // GROOVY-7971: negated || instanceof + @Test + void testInstanceOf20() { + assertScript ''' + void test(Object x) { + if (!(x instanceof String || x instanceof Integer)) { + assert x != null + } + } + test('hello') + test(42) + test(3.14) + ''' + } + + // GROOVY-7971: chained || with 3+ instanceof checks + @Test + void testInstanceOf21() { + assertScript ''' + void test(Object x) { + if (x instanceof String || x instanceof Integer || x instanceof List) { + assert "$x" != null // should be String|Integer|List + } + } + test('hello') + test(42) + test([1, 2]) + ''' + } + + // GROOVY-7971: RHS of || should not see LHS instanceof narrowing + @Test + void testInstanceOf22() { + assertScript ''' + void test(Number n) { + if (n instanceof Integer || n.doubleValue() > 0) { + assert "$n" != null // n should be Integer|Number + } + } + test(42) + test(1.5) + ''' + } + + // GROOVY-7971: closure shared variable with || instanceof + @Test + void testInstanceOf23() { + assertScript ''' + void test(Object x) { + if (x instanceof String || x instanceof Integer) { + def c = { -> x.toString() } + assert c() != null + } + } + test('hello') + test(42) + ''' + } + + // GROOVY-11888: method resolution on union type — toString() is on Object + // and should be found via (String|List) union + @Test + void testInstanceOf24() { + assertScript ''' + void test(Object x) { + if (x instanceof String || x instanceof List) { + assert x.toString() != null + } + } + test('hello') + test([1, 2]) + ''' + } + + // GROOVY-7971: negated || instanceof — re-check instanceof in else branch + @Test @org.junit.jupiter.api.Disabled('requires instanceof compatibility fix for UnionTypeClassNode') + void testInstanceOf25() { + assertScript ''' + void test(Object x) { + if (!(x instanceof String || x instanceof Integer)) { + assert x != null + } else { + assert x instanceof String || x instanceof Integer + } + } + test('hello') + test(42) + test(3.14) + ''' + } + + // GROOVY-11888: method resolution on union type — intValue() is on Number + // and should be found via (Integer|Number) union + @Test + void testInstanceOf26() { + assertScript ''' + void test(Number n) { + if (n instanceof Integer || n.intValue() > 0) { + assert n.intValue() >= 0 + } + } + test(42) + test(1.5) + ''' + } + // GROOVY-5226 @Test void testNestedInstanceOf1() {
