From e29c66d2388665ef60f6fbd005a07c7ffca33c84 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Tue, 11 Nov 2025 22:07:28 +0900 Subject: [PATCH 1/2] Add test for #24074 --- tests/pos/i24074/JavaPart.java | 17 +++++++++++++++++ tests/pos/i24074/Test.scala | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 tests/pos/i24074/JavaPart.java create mode 100644 tests/pos/i24074/Test.scala diff --git a/tests/pos/i24074/JavaPart.java b/tests/pos/i24074/JavaPart.java new file mode 100644 index 000000000000..1a28b7e411c2 --- /dev/null +++ b/tests/pos/i24074/JavaPart.java @@ -0,0 +1,17 @@ +public class JavaPart { + public interface A { } + public interface B extends A { + int onlyInB(); + } + + public interface Lvl1 { + A[] getData(); + } + + public interface Lvl2 extends Lvl1 { + @Override + B[] getData(); + } + + public interface Lvl3 extends Lvl2, Lvl1 { } +} diff --git a/tests/pos/i24074/Test.scala b/tests/pos/i24074/Test.scala new file mode 100644 index 000000000000..7fd5a10330e6 --- /dev/null +++ b/tests/pos/i24074/Test.scala @@ -0,0 +1,3 @@ +object Test: + def test(lvl3: JavaPart.Lvl3): Unit = + lvl3.getData.head.onlyInB() From 5e43e348808a1bcf92507f0969b678db77d2cdf7 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Thu, 13 Nov 2025 15:14:47 +0900 Subject: [PATCH 2/2] Fix override checking for Java methods with covariant array Fixes #24074 When resolving overloads from Java interfaces, treat arrays as covariant. This fixes incorrect method selection when multiple Java interfaces override methods with array return types. Previously, in the example below, we get the compilation error: ``` value foo is not a member of org.test.Test2.A lvl3.foo.head.foo() ``` ```java public class JavaPart { public interface A { } public interface B extends A { int onlyInB(); } public interface Lvl1 { A[] getData(); } public interface Lvl2 extends Lvl1 { @Override B[] getData(); } public interface Lvl3 extends Lvl2, Lvl1 { } } ``` ```scala def test(lvl3: JavaPart.Lvl3): Unit = lvl3.getData.head.onlyInB() ``` because `Denotations#mergeSingleDenot` creates a `JointRefDenotation` for `Lvl1.getData: A[]` and `Lvl2.getData: B[]`, with a return type of `JArray[A] & JArray[B]` (since the compiler doesn't recognize that `Lvl2.getData` overrides `Lvl1.getData`). And because `JArray` isn't recognized as covariant, `JArray[A & B] <: JArray[A] & JArray[B]` cannot be derived. Consequently, `lvl3.getData.head` returns a value typed as `A` instead of neither `B` nor `A & B`, which fails to resolve the method `onlyInB`. --- .../dotty/tools/dotc/core/Denotations.scala | 9 ++++++-- .../dotty/tools/dotc/core/TypeComparer.scala | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index 834e6ae15dfe..03152ab7e291 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -480,9 +480,14 @@ object Denotations { val matchLoosely = sym1.matchNullaryLoosely || sym2.matchNullaryLoosely - if symScore <= 0 && info2.overrides(info1, matchLoosely, checkClassInfo = false) then + val compareCtx = + if sym1.is(JavaDefined) && sym2.is(JavaDefined) then + ctx.withProperty(TypeComparer.ComparingJavaMethods, Some(())) + else ctx + + if symScore <= 0 && info2.overrides(info1, matchLoosely, checkClassInfo = false)(using compareCtx) then denot2 - else if symScore >= 0 && info1.overrides(info2, matchLoosely, checkClassInfo = false) then + else if symScore >= 0 && info1.overrides(info2, matchLoosely, checkClassInfo = false)(using compareCtx) then denot1 else val jointInfo = infoMeet(info1, info2, safeIntersection) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 77fdc24a01cc..2288c9feb059 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -993,7 +993,20 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling (sym1 eq NullClass) && isNullable(tp2) } case tp1 @ AppliedType(tycon1, args1) => - compareAppliedType1(tp1, tycon1, args1) + // Special case: Java arrays are covariant. + // When checking overrides (frozenConstraint) of Java methods, allow B[] <: A[] if B <: A. + def checkJavaArrayCovariance: Boolean = tp2 match { + case AppliedType(tycon2, arg2 :: Nil) + if frozenConstraint + && tycon1.typeSymbol == defn.ArrayClass + && tycon2.typeSymbol == defn.ArrayClass + && args1.length == 1 => + // Arrays are covariant in Java: B[] <: A[] if B <: A + isSubType(args1.head, arg2) + case _ => false + } + (ctx.property(ComparingJavaMethods).isDefined && checkJavaArrayCovariance) + || compareAppliedType1(tp1, tycon1, args1) case tp1: SingletonType => def comparePaths = tp2 match case tp2: (TermRef | ThisType) => @@ -3356,6 +3369,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling object TypeComparer { + import util.Property + + /** A property key to indicate we're comparing Java-defined methods. + * When it is set, arrays are treated as covariant for override checking. + */ + val ComparingJavaMethods = new Property.Key[Unit] + /** A richer compare result, returned by `testSubType` and `test`. */ enum CompareResult: case OK, OKwithGADTUsed, OKwithOpaquesUsed