Skip to content

Commit d362492

Browse files
authored
Dealias before checking for member in lint (#22708)
Fixes #22705 Fixes #22706 Fixes #22727 Follow-up to #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.
1 parent 40ca110 commit d362492

File tree

7 files changed

+106
-30
lines changed

7 files changed

+106
-30
lines changed

compiler/src/dotty/tools/dotc/typer/RefChecks.scala

+27-28
Original file line numberDiff line numberDiff line change
@@ -1137,14 +1137,24 @@ object RefChecks {
11371137
end checkUnaryMethods
11381138

11391139
/** Check that an extension method is not hidden, i.e., that it is callable as an extension method.
1140+
*
1141+
* For example, it is not possible to define a type-safe extension `contains` for `Set`,
1142+
* since for any parameter type, the existing `contains` method will compile and would be used.
11401143
*
11411144
* An extension method is hidden if it does not offer a parameter that is not subsumed
11421145
* by the corresponding parameter of the member with the same name (or of all alternatives of an overload).
11431146
*
1144-
* This check is suppressed if this method is an override.
1147+
* This check is suppressed if the method is an override. (Because the type of the receiver
1148+
* may be narrower in the override.)
11451149
*
1146-
* For example, it is not possible to define a type-safe extension `contains` for `Set`,
1147-
* since for any parameter type, the existing `contains` method will compile and would be used.
1150+
* If the extension method is nullary, it is always hidden by a member of the same name.
1151+
* (Either the member is nullary, or the reference is taken as the eta-expansion of the member.)
1152+
*
1153+
* This check is in lieu of a more expensive use-site check that an application failed to use an extension.
1154+
* That check would account for accessibility and opacity. As a limitation, this check considers
1155+
* only public members for which corresponding method parameters are either both opaque types or both not.
1156+
* It is intended to warn if the receiver type from a third-party library has been augmented with a member
1157+
* that nullifies an existing extension.
11481158
*
11491159
* If the member has a leading implicit parameter list, then the extension method must also have
11501160
* a leading implicit parameter list. The reason is that if the implicit arguments are inferred,
@@ -1155,42 +1165,31 @@ object RefChecks {
11551165
* If the member does not have a leading implicit parameter list, then the argument cannot be explicitly
11561166
* supplied with `using`, as typechecking would fail. But the extension method may have leading implicit
11571167
* parameters, which are necessarily supplied implicitly in the application. The first non-implicit
1158-
* parameters of the extension method must be distinguishable from the member parameters, as described.
1159-
*
1160-
* If the extension method is nullary, it is always hidden by a member of the same name.
1161-
* (Either the member is nullary, or the reference is taken as the eta-expansion of the member.)
1162-
*
1163-
* This check is in lieu of a more expensive use-site check that an application failed to use an extension.
1164-
* That check would account for accessibility and opacity. As a limitation, this check considers
1165-
* only public members, a target receiver that is not an alias, and corresponding method parameters
1166-
* that are either both opaque types or both not.
1168+
* parameters of the extension method must be distinguishable from the member parameters, as described above.
11671169
*/
11681170
def checkExtensionMethods(sym: Symbol)(using Context): Unit =
11691171
if sym.is(Extension) then
11701172
extension (tp: Type)
11711173
def explicit = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true)
11721174
def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false }
11731175
val explicitInfo = sym.info.explicit // consider explicit value params
1174-
val target = explicitInfo.firstParamTypes.head.typeSymbol.info // required for extension method, the putative receiver
1176+
val target0 = explicitInfo.firstParamTypes.head // required for extension method, the putative receiver
1177+
val target = target0.dealiasKeepOpaques.typeSymbol.info
11751178
val methTp = explicitInfo.resultType // skip leading implicits and the "receiver" parameter
1179+
def memberMatchesMethod(member: Denotation) =
1180+
val memberIsImplicit = member.info.hasImplicitParams
1181+
val paramTps =
1182+
if memberIsImplicit then methTp.stripPoly.firstParamTypes
1183+
else methTp.explicit.firstParamTypes
1184+
inline def paramsCorrespond =
1185+
val memberParamTps = member.info.stripPoly.firstParamTypes
1186+
memberParamTps.corresponds(paramTps): (m, x) =>
1187+
m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias && (x frozen_<:< m)
1188+
paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || paramsCorrespond
11761189
def hidden =
11771190
target.nonPrivateMember(sym.name)
11781191
.filterWithPredicate: member =>
1179-
member.symbol.isPublic && {
1180-
val memberIsImplicit = member.info.hasImplicitParams
1181-
val paramTps =
1182-
if memberIsImplicit then methTp.stripPoly.firstParamTypes
1183-
else methTp.explicit.firstParamTypes
1184-
1185-
paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || {
1186-
val memberParamTps = member.info.stripPoly.firstParamTypes
1187-
!memberParamTps.isEmpty
1188-
&& memberParamTps.lengthCompare(paramTps) == 0
1189-
&& memberParamTps.lazyZip(paramTps).forall: (m, x) =>
1190-
m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias
1191-
&& (x frozen_<:< m)
1192-
}
1193-
}
1192+
member.symbol.isPublic && memberMatchesMethod(member)
11941193
.exists
11951194
if sym.is(HasDefaultParams) then
11961195
val getterDenot =

tests/warn/ext-override.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -Xfatal-warnings
1+
//> using options -Werror
22

33
trait Foo[T]:
44
extension (x: T)

tests/warn/i16743.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ trait DungeonDweller:
6666
trait SadDungeonDweller:
6767
def f[A](x: Dungeon.IArray[A]) = 27 // x.length // just to confirm, length is not a member
6868

69-
trait Quote:
69+
trait Quote: // see tests/warn/ext-override.scala
7070
type Tree <: AnyRef
7171
given TreeMethods: TreeMethods
7272
trait TreeMethods:

tests/warn/i22232.scala

+5
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ object Upperbound3:
2323
object NonUpperbound1:
2424
opaque type MyString[+T] = String
2525
extension (arr: MyString[Byte]) def length: Int = 0 // nowarn
26+
2627
object NonUpperbound2:
2728
opaque type MyString[+T] = String
2829
extension [T <: MyString[Byte]](arr: T) def length2: Int = 0 // nowarn
2930

3031
object NonUpperbound3:
3132
opaque type MyString[+T] = String
3233
extension [T](arr: T) def length: Int = 0 // nowarn
34+
35+
object NonUpperbound4:
36+
opaque type MyString = String
37+
extension (arr: MyString) def length: Int = 0 // nowarn

tests/warn/i22705.scala

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//> using options -Werror
2+
3+
object Native {
4+
class Obj:
5+
def f: String = "F"
6+
}
7+
8+
object Types {
9+
10+
opaque type Node = Native.Obj
11+
12+
type S = Node
13+
14+
object S:
15+
def apply(): S = new Node
16+
17+
extension (s: S)
18+
def f: String = "S"
19+
}
20+
21+
import Types.*
22+
23+
object Main {
24+
def main(args: Array[String]): Unit = {
25+
val v: S = S()
26+
println(v.f)
27+
}
28+
}

tests/warn/i22706.scala

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//> using options -Werror
2+
3+
object Native {
4+
class O {
5+
def f: String = "F"
6+
}
7+
class M extends O
8+
}
9+
10+
object Types {
11+
opaque type N = Native.O
12+
opaque type GS = Native.M
13+
14+
type S = N | GS
15+
16+
object S:
17+
def apply(): S = new N
18+
19+
extension (s: S)
20+
def f: String = "S"
21+
}
22+
23+
import Types.*
24+
25+
object Main {
26+
def main(args: Array[String]): Unit = {
27+
val v: S = S()
28+
println(v.f)
29+
}
30+
}

tests/warn/i22727.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//> using options -Werror
2+
3+
object Main {
4+
type IXY = (Int, Int)
5+
6+
extension (xy: IXY) {
7+
def map(f: Int => Int): (Int, Int) = (f(xy._1), f(xy._2))
8+
}
9+
10+
def main(args: Array[String]): Unit = {
11+
val a = (0, 1)
12+
println(a)
13+
}
14+
}

0 commit comments

Comments
 (0)