From f233d896b94323acf7d15c0e9fdf8996a1854608 Mon Sep 17 00:00:00 2001 From: som-snytt Date: Mon, 10 Mar 2025 16:24:24 -0700 Subject: [PATCH] Dealias before checking for member in lint (#22708) Fixes #22705 Fixes #22706 Fixes #22727 Follow-up to https://github.com/scala/scala3/pull/22502 by inserting a `dealias` when arriving at `target` type. Refactored the body of `hidden` to make it easier to read. Adjusted the doc for the same reason. As a reminder to self, the original reason for special handling of aliases was due to subclassing, but overrides are excluded. (One could restore that warning for edge cases.) The long doc explaining the handling of leading implicits is moved to the end (as an appendix). Despite best efforts, I was unable to make the doc longer than the code. [Cherry-picked d36249284e79f33a897e3e53ea3967606f09c40c] --- .../dotty/tools/dotc/typer/RefChecks.scala | 55 +++++++++---------- tests/warn/ext-override.scala | 2 +- tests/warn/i16743.scala | 2 +- tests/warn/i22232.scala | 5 ++ tests/warn/i22705.scala | 28 ++++++++++ tests/warn/i22706.scala | 30 ++++++++++ tests/warn/i22727.scala | 14 +++++ 7 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 tests/warn/i22705.scala create mode 100644 tests/warn/i22706.scala create mode 100644 tests/warn/i22727.scala diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 70daeb600708..3393378617d6 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -1033,14 +1033,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, @@ -1051,15 +1061,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 @@ -1067,26 +1069,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 d08439e13c9a..32277083bd60 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) + } +}