Skip to content

Commit 939f73e

Browse files
authored
Warn if extension receiver already has member (#17543)
Best effort to warn if defining extension method for a type which has a non-private member of the same name and a subsuming signature. Excludes opaque types (for defining forwarders to a public API) and other alias types (where the type member may override an abstract type member). Fixes #16743
2 parents 73882c5 + fdb33a8 commit 939f73e

File tree

13 files changed

+315
-36
lines changed

13 files changed

+315
-36
lines changed

Diff for: compiler/src/dotty/tools/dotc/core/SymDenotations.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1355,7 +1355,7 @@ object SymDenotations {
13551355
* @param inClass The class containing the result symbol's definition
13561356
* @param site The base type from which member types are computed
13571357
*
1358-
* inClass <-- find denot.symbol class C { <-- symbol is here
1358+
* inClass <-- find denot.symbol class C { <-- symbol is here }
13591359
*
13601360
* site: Subtype of both inClass and C
13611361
*/

Diff for: compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
207207
case MatchTypeLegacyPatternID // errorNumber: 191
208208
case UnstableInlineAccessorID // errorNumber: 192
209209
case VolatileOnValID // errorNumber: 193
210+
case ExtensionNullifiedByMemberID // errorNumber: 194
210211

211212
def errorNumber = ordinal - 1
212213

Diff for: compiler/src/dotty/tools/dotc/reporting/messages.scala

+11
Original file line numberDiff line numberDiff line change
@@ -2451,6 +2451,17 @@ class SynchronizedCallOnBoxedClass(stat: tpd.Tree)(using Context)
24512451
|you intended."""
24522452
}
24532453

2454+
class ExtensionNullifiedByMember(method: Symbol, target: Symbol)(using Context)
2455+
extends Message(ExtensionNullifiedByMemberID):
2456+
def kind = MessageKind.PotentialIssue
2457+
def msg(using Context) =
2458+
i"""Extension method ${hl(method.name.toString)} will never be selected
2459+
|because ${hl(target.name.toString)} already has a member with the same name and compatible parameter types."""
2460+
def explain(using Context) =
2461+
i"""An extension method can be invoked as a regular method, but if that is intended,
2462+
|it should not be defined as an extension.
2463+
|Although extensions can be overloaded, they do not overload existing member methods."""
2464+
24542465
class TraitCompanionWithMutableStatic()(using Context)
24552466
extends SyntaxMsg(TraitCompanionWithMutableStaticID) {
24562467
def msg(using Context) = i"Companion of traits cannot define mutable @static fields"

Diff for: compiler/src/dotty/tools/dotc/typer/Applications.scala

+16-16
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,22 @@ object Applications {
346346
val flags2 = sym1.flags | NonMember // ensures Select typing doesn't let TermRef#withPrefix revert the type
347347
val sym2 = sym1.copy(info = methType, flags = flags2) // symbol not entered, to avoid overload resolution problems
348348
fun.withType(sym2.termRef)
349+
350+
/** Drop any leading implicit parameter sections */
351+
def stripImplicit(tp: Type, wildcardOnly: Boolean = false)(using Context): Type = tp match {
352+
case mt: MethodType if mt.isImplicitMethod =>
353+
stripImplicit(resultTypeApprox(mt, wildcardOnly))
354+
case pt: PolyType =>
355+
pt.derivedLambdaType(pt.paramNames, pt.paramInfos,
356+
stripImplicit(pt.resultType, wildcardOnly = true))
357+
// can't use TypeParamRefs for parameter references in `resultTypeApprox`
358+
// since their bounds can refer to type parameters in `pt` that are not
359+
// bound by the constraint. This can lead to hygiene violations if subsequently
360+
// `pt` itself is added to the constraint. Test case is run/enrich-gentraversable.scala.
361+
.asInstanceOf[PolyType].flatten
362+
case _ =>
363+
tp
364+
}
349365
}
350366

351367
trait Applications extends Compatibility {
@@ -1591,22 +1607,6 @@ trait Applications extends Compatibility {
15911607
tp
15921608
}
15931609

1594-
/** Drop any leading implicit parameter sections */
1595-
def stripImplicit(tp: Type, wildcardOnly: Boolean = false)(using Context): Type = tp match {
1596-
case mt: MethodType if mt.isImplicitMethod =>
1597-
stripImplicit(resultTypeApprox(mt, wildcardOnly))
1598-
case pt: PolyType =>
1599-
pt.derivedLambdaType(pt.paramNames, pt.paramInfos,
1600-
stripImplicit(pt.resultType, wildcardOnly = true))
1601-
// can't use TypeParamRefs for parameter references in `resultTypeApprox`
1602-
// since their bounds can refer to type parameters in `pt` that are not
1603-
// bound by the constraint. This can lead to hygiene violations if subsequently
1604-
// `pt` itself is added to the constraint. Test case is run/enrich-gentraversable.scala.
1605-
.asInstanceOf[PolyType].flatten
1606-
case _ =>
1607-
tp
1608-
}
1609-
16101610
/** Compare owner inheritance level.
16111611
* @param sym1 The first owner
16121612
* @param sym2 The second owner

Diff for: compiler/src/dotty/tools/dotc/typer/RefChecks.scala

+56-6
Original file line numberDiff line numberDiff line change
@@ -1033,8 +1033,7 @@ object RefChecks {
10331033
* surprising names at runtime. E.g. in neg/i4564a.scala, a private
10341034
* case class `apply` method would have to be renamed to something else.
10351035
*/
1036-
def checkNoPrivateOverrides(tree: Tree)(using Context): Unit =
1037-
val sym = tree.symbol
1036+
def checkNoPrivateOverrides(sym: Symbol)(using Context): Unit =
10381037
if sym.maybeOwner.isClass
10391038
&& sym.is(Private)
10401039
&& (sym.isOneOf(MethodOrLazyOrMutable) || !sym.is(Local)) // in these cases we'll produce a getter later
@@ -1100,6 +1099,55 @@ object RefChecks {
11001099

11011100
end checkUnaryMethods
11021101

1102+
/** Check that an extension method is not hidden, i.e., that it is callable as an extension method.
1103+
*
1104+
* An extension method is hidden if it does not offer a parameter that is not subsumed
1105+
* by the corresponding parameter of the member with the same name (or of all alternatives of an overload).
1106+
*
1107+
* For example, it is not possible to define a type-safe extension `contains` for `Set`,
1108+
* since for any parameter type, the existing `contains` method will compile and would be used.
1109+
*
1110+
* If the member has a leading implicit parameter list, then the extension method must also have
1111+
* a leading implicit parameter list. The reason is that if the implicit arguments are inferred,
1112+
* either the member method is used or typechecking fails. If the implicit arguments are supplied
1113+
* explicitly and the member method is not applicable, the extension is checked, and its parameters
1114+
* must be implicit in order to be applicable.
1115+
*
1116+
* If the member does not have a leading implicit parameter list, then the argument cannot be explicitly
1117+
* supplied with `using`, as typechecking would fail. But the extension method may have leading implicit
1118+
* parameters, which are necessarily supplied implicitly in the application. The first non-implicit
1119+
* parameters of the extension method must be distinguishable from the member parameters, as described.
1120+
*
1121+
* If the extension method is nullary, it is always hidden by a member of the same name.
1122+
* (Either the member is nullary, or the reference is taken as the eta-expansion of the member.)
1123+
*/
1124+
def checkExtensionMethods(sym: Symbol)(using Context): Unit = if sym.is(Extension) then
1125+
extension (tp: Type)
1126+
def strippedResultType = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).resultType
1127+
def firstExplicitParamTypes = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).firstParamTypes
1128+
def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false }
1129+
val target = sym.info.firstExplicitParamTypes.head // required for extension method, the putative receiver
1130+
val methTp = sym.info.strippedResultType // skip leading implicits and the "receiver" parameter
1131+
def hidden =
1132+
target.nonPrivateMember(sym.name)
1133+
.filterWithPredicate:
1134+
member =>
1135+
val memberIsImplicit = member.info.hasImplicitParams
1136+
val paramTps =
1137+
if memberIsImplicit then methTp.stripPoly.firstParamTypes
1138+
else methTp.firstExplicitParamTypes
1139+
1140+
paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || {
1141+
val memberParamTps = member.info.stripPoly.firstParamTypes
1142+
!memberParamTps.isEmpty
1143+
&& memberParamTps.lengthCompare(paramTps) == 0
1144+
&& memberParamTps.lazyZip(paramTps).forall((m, x) => x frozen_<:< m)
1145+
}
1146+
.exists
1147+
if !target.typeSymbol.denot.isAliasType && !target.typeSymbol.denot.isOpaqueAlias && hidden
1148+
then report.warning(ExtensionNullifiedByMember(sym, target.typeSymbol), sym.srcPos)
1149+
end checkExtensionMethods
1150+
11031151
/** Verify that references in the user-defined `@implicitNotFound` message are valid.
11041152
* (i.e. they refer to a type variable that really occurs in the signature of the annotated symbol.)
11051153
*/
@@ -1233,8 +1281,8 @@ class RefChecks extends MiniPhase { thisPhase =>
12331281

12341282
override def transformValDef(tree: ValDef)(using Context): ValDef = {
12351283
if tree.symbol.exists then
1236-
checkNoPrivateOverrides(tree)
12371284
val sym = tree.symbol
1285+
checkNoPrivateOverrides(sym)
12381286
checkVolatile(sym)
12391287
if (sym.exists && sym.owner.isTerm) {
12401288
tree.rhs match {
@@ -1246,9 +1294,11 @@ class RefChecks extends MiniPhase { thisPhase =>
12461294
}
12471295

12481296
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
1249-
checkNoPrivateOverrides(tree)
1250-
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
1251-
checkUnaryMethods(tree.symbol)
1297+
val sym = tree.symbol
1298+
checkNoPrivateOverrides(sym)
1299+
checkImplicitNotFoundAnnotation.defDef(sym.denot)
1300+
checkUnaryMethods(sym)
1301+
checkExtensionMethods(sym)
12521302
tree
12531303
}
12541304

Diff for: compiler/src/dotty/tools/dotc/typer/Typer.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -2568,17 +2568,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25682568
vdef1.setDefTree
25692569
}
25702570

2571-
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = {
2572-
def canBeInvalidated(sym: Symbol): Boolean =
2571+
private def retractDefDef(sym: Symbol)(using Context): Tree =
2572+
// it's a discarded method (synthetic case class method or synthetic java record constructor or overridden member), drop it
2573+
val canBeInvalidated: Boolean =
25732574
sym.is(Synthetic)
25742575
&& (desugar.isRetractableCaseClassMethodName(sym.name) ||
25752576
(sym.owner.is(JavaDefined) && sym.owner.derivesFrom(defn.JavaRecordClass) && sym.is(Method)))
2577+
assert(canBeInvalidated)
2578+
sym.owner.info.decls.openForMutations.unlink(sym)
2579+
EmptyTree
25762580

2577-
if !sym.info.exists then
2578-
// it's a discarded method (synthetic case class method or synthetic java record constructor or overriden member), drop it
2579-
assert(canBeInvalidated(sym))
2580-
sym.owner.info.decls.openForMutations.unlink(sym)
2581-
return EmptyTree
2581+
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if !sym.info.exists then retractDefDef(sym) else {
25822582

25832583
// TODO: - Remove this when `scala.language.experimental.erasedDefinitions` is no longer experimental.
25842584
// - Modify signature to `erased def erasedValue[T]: T`

Diff for: compiler/test/dotty/tools/dotc/printing/PrintingTest.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import java.io.File
2525
class PrintingTest {
2626

2727
def options(phase: String, flags: List[String]) =
28-
List(s"-Xprint:$phase", "-color:never", "-classpath", TestConfiguration.basicClasspath) ::: flags
28+
List(s"-Xprint:$phase", "-color:never", "-nowarn", "-classpath", TestConfiguration.basicClasspath) ::: flags
2929

3030
private def compileFile(path: JPath, phase: String): Boolean = {
3131
val baseFilePath = path.toString.stripSuffix(".scala")

Diff for: compiler/test/dotty/tools/scripting/ScriptTestEnv.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,10 @@ object ScriptTestEnv {
217217

218218
def toUrl: String = Paths.get(absPath).toUri.toURL.toString
219219

220+
// Used to be an extension on String
220221
// Treat norm paths with a leading '/' as absolute (Windows java.io.File#isAbsolute treats them as relative)
221-
def isAbsolute = p.norm.startsWith("/") || (isWin && p.norm.secondChar == ":")
222+
//@annotation.nowarn // hidden by Path#isAbsolute
223+
//def isAbsolute = p.norm.startsWith("/") || (isWin && p.norm.secondChar == ":")
222224
}
223225

224226
extension(f: File) {

Diff for: scaladoc-testcases/src/tests/implicitConversions.scala

+5-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ given Conversion[A, B] with {
66
def apply(a: A): B = ???
77
}
88

9-
extension (a: A) def extended_bar(): String = ???
9+
extension (a: A)
10+
@annotation.nowarn
11+
def extended_bar(): String = ???
1012

1113
class A {
1214
implicit def conversion(c: C): D = ???
@@ -45,7 +47,7 @@ class B {
4547
class C {
4648
def extensionInCompanion: String = ???
4749
}
48-
50+
@annotation.nowarn // extensionInCompanion
4951
object C {
5052
implicit def companionConversion(c: C): B = ???
5153

@@ -70,4 +72,4 @@ package nested {
7072
}
7173

7274
class Z
73-
}
75+
}

Diff for: scaladoc-testcases/src/tests/inheritedMembers1.scala

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tests
22
package inheritedMembers1
33

44

5+
/*<-*/@annotation.nowarn/*->*/
56
class A
67
{
78
def A: String

Diff for: tests/warn/i16743.check

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:30:6 --------------------------------------------------------
2+
30 | def t = 27 // warn
3+
| ^
4+
| Extension method t will never be selected
5+
| because T already has a member with the same name and compatible parameter types.
6+
|
7+
| longer explanation available when compiling with `-explain`
8+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:32:6 --------------------------------------------------------
9+
32 | def g(x: String)(i: Int): String = x*i // warn
10+
| ^
11+
| Extension method g will never be selected
12+
| because T already has a member with the same name and compatible parameter types.
13+
|
14+
| longer explanation available when compiling with `-explain`
15+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:33:6 --------------------------------------------------------
16+
33 | def h(x: String): String = x // warn
17+
| ^
18+
| Extension method h will never be selected
19+
| because T already has a member with the same name and compatible parameter types.
20+
|
21+
| longer explanation available when compiling with `-explain`
22+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:35:6 --------------------------------------------------------
23+
35 | def j(x: Any, y: Int): String = (x.toString)*y // warn
24+
| ^
25+
| Extension method j will never be selected
26+
| because T already has a member with the same name and compatible parameter types.
27+
|
28+
| longer explanation available when compiling with `-explain`
29+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:36:6 --------------------------------------------------------
30+
36 | def k(x: String): String = x // warn
31+
| ^
32+
| Extension method k will never be selected
33+
| because T already has a member with the same name and compatible parameter types.
34+
|
35+
| longer explanation available when compiling with `-explain`
36+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:38:6 --------------------------------------------------------
37+
38 | def m(using String): String = "m" + summon[String] // warn
38+
| ^
39+
| Extension method m will never be selected
40+
| because T already has a member with the same name and compatible parameter types.
41+
|
42+
| longer explanation available when compiling with `-explain`
43+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:39:6 --------------------------------------------------------
44+
39 | def n(using String): String = "n" + summon[String] // warn
45+
| ^
46+
| Extension method n will never be selected
47+
| because T already has a member with the same name and compatible parameter types.
48+
|
49+
| longer explanation available when compiling with `-explain`
50+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:40:6 --------------------------------------------------------
51+
40 | def o: String = "42" // warn
52+
| ^
53+
| Extension method o will never be selected
54+
| because T already has a member with the same name and compatible parameter types.
55+
|
56+
| longer explanation available when compiling with `-explain`
57+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:41:6 --------------------------------------------------------
58+
41 | def u: Int = 27 // warn
59+
| ^
60+
| Extension method u will never be selected
61+
| because T already has a member with the same name and compatible parameter types.
62+
|
63+
| longer explanation available when compiling with `-explain`
64+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:44:6 --------------------------------------------------------
65+
44 | def at: Int = 42 // warn
66+
| ^
67+
| Extension method at will never be selected
68+
| because T already has a member with the same name and compatible parameter types.
69+
|
70+
| longer explanation available when compiling with `-explain`
71+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:46:6 --------------------------------------------------------
72+
46 | def x(using String)(n: Int): Int = summon[String].toInt + n // warn
73+
| ^
74+
| Extension method x will never be selected
75+
| because T already has a member with the same name and compatible parameter types.
76+
|
77+
| longer explanation available when compiling with `-explain`
78+
-- [E194] Potential Issue Warning: tests/warn/i16743.scala:47:6 --------------------------------------------------------
79+
47 | def y(using String)(s: String): String = s + summon[String] // warn
80+
| ^
81+
| Extension method y will never be selected
82+
| because T already has a member with the same name and compatible parameter types.
83+
|
84+
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)