Skip to content

Commit ba254b9

Browse files
Backport "Warn if extension receiver already has member" to LTS (#21052)
Backports #17543 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents 5e54866 + 43ca7b9 commit ba254b9

File tree

13 files changed

+315
-36
lines changed

13 files changed

+315
-36
lines changed

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1349,7 +1349,7 @@ object SymDenotations {
13491349
* @param inClass The class containing the result symbol's definition
13501350
* @param site The base type from which member types are computed
13511351
*
1352-
* inClass <-- find denot.symbol class C { <-- symbol is here
1352+
* inClass <-- find denot.symbol class C { <-- symbol is here }
13531353
*
13541354
* site: Subtype of both inClass and C
13551355
*/

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 - unused in LTS
208208
case UnstableInlineAccessorID // errorNumber: 192 - unused in LTS
209209
case VolatileOnValID // errorNumber: 193
210+
case ExtensionNullifiedByMemberID // errorNumber: 194
210211

211212
def errorNumber = ordinal - 1
212213

compiler/src/dotty/tools/dotc/reporting/messages.scala

+11
Original file line numberDiff line numberDiff line change
@@ -2425,6 +2425,17 @@ class SynchronizedCallOnBoxedClass(stat: tpd.Tree)(using Context)
24252425
|you intended."""
24262426
}
24272427

2428+
class ExtensionNullifiedByMember(method: Symbol, target: Symbol)(using Context)
2429+
extends Message(ExtensionNullifiedByMemberID):
2430+
def kind = MessageKind.PotentialIssue
2431+
def msg(using Context) =
2432+
i"""Extension method ${hl(method.name.toString)} will never be selected
2433+
|because ${hl(target.name.toString)} already has a member with the same name and compatible parameter types."""
2434+
def explain(using Context) =
2435+
i"""An extension method can be invoked as a regular method, but if that is intended,
2436+
|it should not be defined as an extension.
2437+
|Although extensions can be overloaded, they do not overload existing member methods."""
2438+
24282439
class TraitCompanionWithMutableStatic()(using Context)
24292440
extends SyntaxMsg(TraitCompanionWithMutableStaticID) {
24302441
def msg(using Context) = i"Companion of traits cannot define mutable @static fields"

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 {
@@ -1577,22 +1593,6 @@ trait Applications extends Compatibility {
15771593
tp
15781594
}
15791595

1580-
/** Drop any leading implicit parameter sections */
1581-
def stripImplicit(tp: Type, wildcardOnly: Boolean = false)(using Context): Type = tp match {
1582-
case mt: MethodType if mt.isImplicitMethod =>
1583-
stripImplicit(resultTypeApprox(mt, wildcardOnly))
1584-
case pt: PolyType =>
1585-
pt.derivedLambdaType(pt.paramNames, pt.paramInfos,
1586-
stripImplicit(pt.resultType, wildcardOnly = true))
1587-
// can't use TypeParamRefs for parameter references in `resultTypeApprox`
1588-
// since their bounds can refer to type parameters in `pt` that are not
1589-
// bound by the constraint. This can lead to hygiene violations if subsequently
1590-
// `pt` itself is added to the constraint. Test case is run/enrich-gentraversable.scala.
1591-
.asInstanceOf[PolyType].flatten
1592-
case _ =>
1593-
tp
1594-
}
1595-
15961596
/** Compare owner inheritance level.
15971597
* @param sym1 The first owner
15981598
* @param sym2 The second owner

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

+56-6
Original file line numberDiff line numberDiff line change
@@ -952,8 +952,7 @@ object RefChecks {
952952
* surprising names at runtime. E.g. in neg/i4564a.scala, a private
953953
* case class `apply` method would have to be renamed to something else.
954954
*/
955-
def checkNoPrivateOverrides(tree: Tree)(using Context): Unit =
956-
val sym = tree.symbol
955+
def checkNoPrivateOverrides(sym: Symbol)(using Context): Unit =
957956
if sym.maybeOwner.isClass
958957
&& sym.is(Private)
959958
&& (sym.isOneOf(MethodOrLazyOrMutable) || !sym.is(Local)) // in these cases we'll produce a getter later
@@ -1017,6 +1016,55 @@ object RefChecks {
10171016

10181017
end checkUnaryMethods
10191018

1019+
/** Check that an extension method is not hidden, i.e., that it is callable as an extension method.
1020+
*
1021+
* An extension method is hidden if it does not offer a parameter that is not subsumed
1022+
* by the corresponding parameter of the member with the same name (or of all alternatives of an overload).
1023+
*
1024+
* For example, it is not possible to define a type-safe extension `contains` for `Set`,
1025+
* since for any parameter type, the existing `contains` method will compile and would be used.
1026+
*
1027+
* If the member has a leading implicit parameter list, then the extension method must also have
1028+
* a leading implicit parameter list. The reason is that if the implicit arguments are inferred,
1029+
* either the member method is used or typechecking fails. If the implicit arguments are supplied
1030+
* explicitly and the member method is not applicable, the extension is checked, and its parameters
1031+
* must be implicit in order to be applicable.
1032+
*
1033+
* If the member does not have a leading implicit parameter list, then the argument cannot be explicitly
1034+
* supplied with `using`, as typechecking would fail. But the extension method may have leading implicit
1035+
* parameters, which are necessarily supplied implicitly in the application. The first non-implicit
1036+
* parameters of the extension method must be distinguishable from the member parameters, as described.
1037+
*
1038+
* If the extension method is nullary, it is always hidden by a member of the same name.
1039+
* (Either the member is nullary, or the reference is taken as the eta-expansion of the member.)
1040+
*/
1041+
def checkExtensionMethods(sym: Symbol)(using Context): Unit = if sym.is(Extension) then
1042+
extension (tp: Type)
1043+
def strippedResultType = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).resultType
1044+
def firstExplicitParamTypes = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).firstParamTypes
1045+
def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false }
1046+
val target = sym.info.firstExplicitParamTypes.head // required for extension method, the putative receiver
1047+
val methTp = sym.info.strippedResultType // skip leading implicits and the "receiver" parameter
1048+
def hidden =
1049+
target.nonPrivateMember(sym.name)
1050+
.filterWithPredicate:
1051+
member =>
1052+
val memberIsImplicit = member.info.hasImplicitParams
1053+
val paramTps =
1054+
if memberIsImplicit then methTp.stripPoly.firstParamTypes
1055+
else methTp.firstExplicitParamTypes
1056+
1057+
paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || {
1058+
val memberParamTps = member.info.stripPoly.firstParamTypes
1059+
!memberParamTps.isEmpty
1060+
&& memberParamTps.lengthCompare(paramTps) == 0
1061+
&& memberParamTps.lazyZip(paramTps).forall((m, x) => x frozen_<:< m)
1062+
}
1063+
.exists
1064+
if !target.typeSymbol.denot.isAliasType && !target.typeSymbol.denot.isOpaqueAlias && hidden
1065+
then report.warning(ExtensionNullifiedByMember(sym, target.typeSymbol), sym.srcPos)
1066+
end checkExtensionMethods
1067+
10201068
/** Verify that references in the user-defined `@implicitNotFound` message are valid.
10211069
* (i.e. they refer to a type variable that really occurs in the signature of the annotated symbol.)
10221070
*/
@@ -1150,8 +1198,8 @@ class RefChecks extends MiniPhase { thisPhase =>
11501198

11511199
override def transformValDef(tree: ValDef)(using Context): ValDef = {
11521200
if tree.symbol.exists then
1153-
checkNoPrivateOverrides(tree)
11541201
val sym = tree.symbol
1202+
checkNoPrivateOverrides(sym)
11551203
checkVolatile(sym)
11561204
if (sym.exists && sym.owner.isTerm) {
11571205
tree.rhs match {
@@ -1163,9 +1211,11 @@ class RefChecks extends MiniPhase { thisPhase =>
11631211
}
11641212

11651213
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
1166-
checkNoPrivateOverrides(tree)
1167-
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
1168-
checkUnaryMethods(tree.symbol)
1214+
val sym = tree.symbol
1215+
checkNoPrivateOverrides(sym)
1216+
checkImplicitNotFoundAnnotation.defDef(sym.denot)
1217+
checkUnaryMethods(sym)
1218+
checkExtensionMethods(sym)
11691219
tree
11701220
}
11711221

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -2446,17 +2446,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
24462446
vdef1.setDefTree
24472447
}
24482448

2449-
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = {
2450-
def canBeInvalidated(sym: Symbol): Boolean =
2449+
private def retractDefDef(sym: Symbol)(using Context): Tree =
2450+
// it's a discarded method (synthetic case class method or synthetic java record constructor or overridden member), drop it
2451+
val canBeInvalidated: Boolean =
24512452
sym.is(Synthetic)
24522453
&& (desugar.isRetractableCaseClassMethodName(sym.name) ||
24532454
(sym.owner.is(JavaDefined) && sym.owner.derivesFrom(defn.JavaRecordClass) && sym.is(Method)))
2455+
assert(canBeInvalidated)
2456+
sym.owner.info.decls.openForMutations.unlink(sym)
2457+
EmptyTree
24542458

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

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

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")

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) {

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+
}

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

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)