From 0631ec387f6a6082fa69dbb8ceb41ae5f3fe9dfb Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 5 Aug 2025 07:48:45 -0700 Subject: [PATCH 1/2] Defend against AliasingBounds In refchecks for extension methods, obtain a TypeRef of the receiver parameter type, so that it is correctly dealiased. The prior idiom produced a TypeAlias. --- .../dotty/tools/dotc/typer/RefChecks.scala | 23 ++++++++----------- tests/warn/i23666.scala | 15 ++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 tests/warn/i23666.scala diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 7037a9a986aa..bcc3939850bb 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -1158,17 +1158,12 @@ object RefChecks { */ def checkExtensionMethods(sym: Symbol)(using Context): Unit = if sym.is(Extension) then atPhase(typerPhase): - extension (tp: Type) - def explicit = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true) - def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false } - def isParamLess = tp.stripPoly match { case mt: MethodType => false case _ => true } + extension (tp: Type) def explicit = Applications.stripImplicit(tp.widen, wildcardOnly = true) val explicitInfo = sym.info.explicit // consider explicit value params def memberHidesMethod(member: Denotation): Boolean = - val methTp = explicitInfo.resultType // skip leading implicits and the "receiver" parameter - if methTp.isParamLess then - return true // extension without parens is always hidden by a member of same name - val memberIsImplicit = member.info.hasImplicitParams - inline def paramsCorrespond = + val methTp = explicitInfo.stripPoly.resultType // skip leading implicits and the "receiver" parameter + val memberIsImplicit = member.info.isImplicitMethod + inline def paramsCorrespond = { val paramTps = if memberIsImplicit then methTp.stripPoly.firstParamTypes else methTp.explicit.firstParamTypes @@ -1176,11 +1171,13 @@ object RefChecks { memberParamTps.corresponds(paramTps): (m, x) => m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias && (x frozen_<:< m) - memberIsImplicit && !methTp.hasImplicitParams || paramsCorrespond + } + methTp.isParameterless // extension without parens is always hidden by a member of same name + || memberIsImplicit && !methTp.isImplicitMethod // see above + || paramsCorrespond // match by type and opacity def targetOfHiddenExtension: Symbol = - val target = - val target0 = explicitInfo.firstParamTypes.head // required for extension method, the putative receiver - target0.dealiasKeepOpaques.typeSymbol.info + val target = explicitInfo.firstParamTypes.head // required for extension method; the nominal receiver + .typeSymbol.typeRef.dealiasKeepOpaques val member = target.nonPrivateMember(sym.name) .filterWithPredicate: member => member.symbol.isPublic && memberHidesMethod(member) diff --git a/tests/warn/i23666.scala b/tests/warn/i23666.scala new file mode 100644 index 000000000000..d35fd0633b81 --- /dev/null +++ b/tests/warn/i23666.scala @@ -0,0 +1,15 @@ + +type Tuple = scala.Tuple + +infix type =:= [A, B] = A => B + +object `=:=` : + given [A] => A =:= A = a => a + +extension [T <: Tuple] (tuple: T) + + def reverse[A, B](using ev: T =:= (A, B)): (B, A) = // warn + val ab = ev(tuple) + (ab._2, ab._1) + + def toList: List[Nothing] = Nil // warn From a21bc0cc57ad5bf8b953344210d61e25bb2d851f Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 23 Nov 2025 04:57:57 -0800 Subject: [PATCH 2/2] Consider more upper bounds in extension lint --- .../dotty/tools/dotc/typer/RefChecks.scala | 20 ++++++++++++++----- tests/warn/i22232.check | 11 ++++++++-- tests/warn/i22232.scala | 8 ++++++++ tests/warn/i23293.scala | 3 +++ 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 tests/warn/i23293.scala diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index bcc3939850bb..8083d95c7307 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -1158,26 +1158,36 @@ object RefChecks { */ def checkExtensionMethods(sym: Symbol)(using Context): Unit = if sym.is(Extension) then atPhase(typerPhase): - extension (tp: Type) def explicit = Applications.stripImplicit(tp.widen, wildcardOnly = true) + extension (tp: Type) + def explicit = Applications.stripImplicit(tp.widen, wildcardOnly = true) + def hiBoundOpaque = + if tp.typeSymbol.isOpaqueAlias then + tp.typeSymbol.info match + case TypeBounds(lo, hi) if lo ne hi => hi + case _ => tp + else tp.hiBound val explicitInfo = sym.info.explicit // consider explicit value params def memberHidesMethod(member: Denotation): Boolean = val methTp = explicitInfo.stripPoly.resultType // skip leading implicits and the "receiver" parameter val memberIsImplicit = member.info.isImplicitMethod + // are the params of the extension subsumed by the params of the member? + // an unbounded type param always subsumes. + // the params must have the same opacity to conclude the extension is hidden. inline def paramsCorrespond = { val paramTps = - if memberIsImplicit then methTp.stripPoly.firstParamTypes + if memberIsImplicit then methTp.firstParamTypes else methTp.explicit.firstParamTypes - val memberParamTps = member.info.stripPoly.firstParamTypes + val memberParamTps = member.info.firstParamTypes memberParamTps.corresponds(paramTps): (m, x) => m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias - && (x frozen_<:< m) + && (m.isInstanceOf[ParamRef] && (m eq m.hiBound) || (x.hiBound frozen_<:< m.hiBound)) } methTp.isParameterless // extension without parens is always hidden by a member of same name || memberIsImplicit && !methTp.isImplicitMethod // see above || paramsCorrespond // match by type and opacity def targetOfHiddenExtension: Symbol = val target = explicitInfo.firstParamTypes.head // required for extension method; the nominal receiver - .typeSymbol.typeRef.dealiasKeepOpaques + .hiBound.typeSymbol.typeRef.dealiasKeepOpaques.hiBoundOpaque val member = target.nonPrivateMember(sym.name) .filterWithPredicate: member => member.symbol.isPublic && memberHidesMethod(member) diff --git a/tests/warn/i22232.check b/tests/warn/i22232.check index cf3d6d4e004e..d07f11aaf58e 100644 --- a/tests/warn/i22232.check +++ b/tests/warn/i22232.check @@ -19,8 +19,15 @@ | because String already has a member with the same name and compatible parameter types. | | longer explanation available when compiling with `-explain` --- [E194] Potential Issue Warning: tests/warn/i22232.scala:17:46 ------------------------------------------------------- -17 | extension [T <: MyString[Byte]](arr: T) def length: Int = 0 // warn +-- [E194] Potential Issue Warning: tests/warn/i22232.scala:21:39 ------------------------------------------------------- +21 | extension (arr: MyStuff[String]) def add(s: String): MyStuff[String] = ??? // warn + | ^ + | Extension method add will never be selected from type MyList + | because MyList already has a member with the same name and compatible parameter types. + | + | longer explanation available when compiling with `-explain` +-- [E194] Potential Issue Warning: tests/warn/i22232.scala:25:46 ------------------------------------------------------- +25 | extension [T <: MyString[Byte]](arr: T) def length: Int = 0 // warn | ^ | Extension method length will never be selected from type String | because String already has a member with the same name and compatible parameter types. diff --git a/tests/warn/i22232.scala b/tests/warn/i22232.scala index f94e413920a2..e39610e4666f 100644 --- a/tests/warn/i22232.scala +++ b/tests/warn/i22232.scala @@ -12,6 +12,14 @@ object Upperbound1: opaque type MyString[+T] <: String = String extension (arr: MyString[Byte]) def length: Int = 0 // warn +object Upperbound1a: + trait MyList[+A]: + def add[B >: A](x: B): MyList[B] + class MyListImpl[+A] extends MyList[A]: + def add[B >: A](x: B): MyList[B] = ??? + opaque type MyStuff[+T] <: MyList[T] = MyListImpl[T] + extension (arr: MyStuff[String]) def add(s: String): MyStuff[String] = ??? // warn + object Upperbound2: opaque type MyString[+T] <: String = String extension [T <: MyString[Byte]](arr: T) def length: Int = 0 // warn diff --git a/tests/warn/i23293.scala b/tests/warn/i23293.scala new file mode 100644 index 000000000000..058fd770ea1b --- /dev/null +++ b/tests/warn/i23293.scala @@ -0,0 +1,3 @@ +trait SelectByName[Field <: String & Singleton, Rec <: Tuple]: + type Out + extension (r: Rec) def apply[F <: Field]: Out // warn not crash