diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 629d18168c2c..a015348e90a7 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -1137,14 +1137,24 @@ object RefChecks { end checkUnaryMethods /** Check that an extension method is not hidden, i.e., that it is callable as an extension method. + * + * For example, it is not possible to define a type-safe extension `contains` for `Set`, + * since for any parameter type, the existing `contains` method will compile and would be used. * * An extension method is hidden if it does not offer a parameter that is not subsumed * by the corresponding parameter of the member with the same name (or of all alternatives of an overload). * - * This check is suppressed if this method is an override. + * This check is suppressed if the method is an override. (Because the type of the receiver + * may be narrower in the override.) * - * For example, it is not possible to define a type-safe extension `contains` for `Set`, - * since for any parameter type, the existing `contains` method will compile and would be used. + * If the extension method is nullary, it is always hidden by a member of the same name. + * (Either the member is nullary, or the reference is taken as the eta-expansion of the member.) + * + * This check is in lieu of a more expensive use-site check that an application failed to use an extension. + * That check would account for accessibility and opacity. As a limitation, this check considers + * only public members for which corresponding method parameters are either both opaque types or both not. + * It is intended to warn if the receiver type from a third-party library has been augmented with a member + * that nullifies an existing extension. * * If the member has a leading implicit parameter list, then the extension method must also have * a leading implicit parameter list. The reason is that if the implicit arguments are inferred, @@ -1155,15 +1165,7 @@ object RefChecks { * If the member does not have a leading implicit parameter list, then the argument cannot be explicitly * supplied with `using`, as typechecking would fail. But the extension method may have leading implicit * parameters, which are necessarily supplied implicitly in the application. The first non-implicit - * parameters of the extension method must be distinguishable from the member parameters, as described. - * - * If the extension method is nullary, it is always hidden by a member of the same name. - * (Either the member is nullary, or the reference is taken as the eta-expansion of the member.) - * - * This check is in lieu of a more expensive use-site check that an application failed to use an extension. - * That check would account for accessibility and opacity. As a limitation, this check considers - * only public members, a target receiver that is not an alias, and corresponding method parameters - * that are either both opaque types or both not. + * parameters of the extension method must be distinguishable from the member parameters, as described above. */ def checkExtensionMethods(sym: Symbol)(using Context): Unit = if sym.is(Extension) then @@ -1171,26 +1173,23 @@ object RefChecks { def explicit = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true) def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false } val explicitInfo = sym.info.explicit // consider explicit value params - val target = explicitInfo.firstParamTypes.head.typeSymbol.info // required for extension method, the putative receiver + val target0 = explicitInfo.firstParamTypes.head // required for extension method, the putative receiver + val target = target0.dealiasKeepOpaques.typeSymbol.info val methTp = explicitInfo.resultType // skip leading implicits and the "receiver" parameter + def memberMatchesMethod(member: Denotation) = + val memberIsImplicit = member.info.hasImplicitParams + val paramTps = + if memberIsImplicit then methTp.stripPoly.firstParamTypes + else methTp.explicit.firstParamTypes + inline def paramsCorrespond = + val memberParamTps = member.info.stripPoly.firstParamTypes + memberParamTps.corresponds(paramTps): (m, x) => + m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias && (x frozen_<:< m) + paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || paramsCorrespond def hidden = target.nonPrivateMember(sym.name) .filterWithPredicate: member => - member.symbol.isPublic && { - val memberIsImplicit = member.info.hasImplicitParams - val paramTps = - if memberIsImplicit then methTp.stripPoly.firstParamTypes - else methTp.explicit.firstParamTypes - - paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || { - val memberParamTps = member.info.stripPoly.firstParamTypes - !memberParamTps.isEmpty - && memberParamTps.lengthCompare(paramTps) == 0 - && memberParamTps.lazyZip(paramTps).forall: (m, x) => - m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias - && (x frozen_<:< m) - } - } + member.symbol.isPublic && memberMatchesMethod(member) .exists if sym.is(HasDefaultParams) then val getterDenot = diff --git a/tests/warn/ext-override.scala b/tests/warn/ext-override.scala index 7c082695cbaa..e5933350fdb9 100644 --- a/tests/warn/ext-override.scala +++ b/tests/warn/ext-override.scala @@ -1,4 +1,4 @@ -//> using options -Xfatal-warnings +//> using options -Werror trait Foo[T]: extension (x: T) diff --git a/tests/warn/i16743.scala b/tests/warn/i16743.scala index e8860aeabaae..213e22ff4cb4 100644 --- a/tests/warn/i16743.scala +++ b/tests/warn/i16743.scala @@ -66,7 +66,7 @@ trait DungeonDweller: trait SadDungeonDweller: def f[A](x: Dungeon.IArray[A]) = 27 // x.length // just to confirm, length is not a member -trait Quote: +trait Quote: // see tests/warn/ext-override.scala type Tree <: AnyRef given TreeMethods: TreeMethods trait TreeMethods: diff --git a/tests/warn/i22232.scala b/tests/warn/i22232.scala index 79b8317a7329..f94e413920a2 100644 --- a/tests/warn/i22232.scala +++ b/tests/warn/i22232.scala @@ -23,6 +23,7 @@ object Upperbound3: object NonUpperbound1: opaque type MyString[+T] = String extension (arr: MyString[Byte]) def length: Int = 0 // nowarn + object NonUpperbound2: opaque type MyString[+T] = String extension [T <: MyString[Byte]](arr: T) def length2: Int = 0 // nowarn @@ -30,3 +31,7 @@ object NonUpperbound2: object NonUpperbound3: opaque type MyString[+T] = String extension [T](arr: T) def length: Int = 0 // nowarn + +object NonUpperbound4: + opaque type MyString = String + extension (arr: MyString) def length: Int = 0 // nowarn diff --git a/tests/warn/i22705.scala b/tests/warn/i22705.scala new file mode 100644 index 000000000000..d30c1b310201 --- /dev/null +++ b/tests/warn/i22705.scala @@ -0,0 +1,28 @@ +//> using options -Werror + +object Native { + class Obj: + def f: String = "F" +} + +object Types { + + opaque type Node = Native.Obj + + type S = Node + + object S: + def apply(): S = new Node + + extension (s: S) + def f: String = "S" +} + +import Types.* + +object Main { + def main(args: Array[String]): Unit = { + val v: S = S() + println(v.f) + } +} diff --git a/tests/warn/i22706.scala b/tests/warn/i22706.scala new file mode 100644 index 000000000000..5bd642020e1c --- /dev/null +++ b/tests/warn/i22706.scala @@ -0,0 +1,30 @@ +//> using options -Werror + +object Native { + class O { + def f: String = "F" + } + class M extends O +} + +object Types { + opaque type N = Native.O + opaque type GS = Native.M + + type S = N | GS + + object S: + def apply(): S = new N + + extension (s: S) + def f: String = "S" +} + +import Types.* + +object Main { + def main(args: Array[String]): Unit = { + val v: S = S() + println(v.f) + } +} diff --git a/tests/warn/i22727.scala b/tests/warn/i22727.scala new file mode 100644 index 000000000000..c7b1240c7e6b --- /dev/null +++ b/tests/warn/i22727.scala @@ -0,0 +1,14 @@ +//> using options -Werror + +object Main { + type IXY = (Int, Int) + + extension (xy: IXY) { + def map(f: Int => Int): (Int, Int) = (f(xy._1), f(xy._2)) + } + + def main(args: Array[String]): Unit = { + val a = (0, 1) + println(a) + } +}