Skip to content

Commit 46d6a58

Browse files
committed
Warn if extension is hidden by member of receiver
1 parent 9682751 commit 46d6a58

File tree

13 files changed

+203
-34
lines changed

13 files changed

+203
-34
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,7 @@ object SymDenotations {
13461346
* inClass <-- find denot.symbol class C { <-- symbol is here
13471347
*
13481348
* site: Subtype of both inClass and C
1349+
* } <-- balance the brace
13491350
*/
13501351
final def matchingDecl(inClass: Symbol, site: Type, name: Name = this.name)(using Context): Symbol = {
13511352
var denot = inClass.info.nonPrivateDecl(name)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
206206
case PureUnitExpressionID // errorNumber: 190
207207
case MatchTypeLegacyPatternID // errorNumber: 191
208208
case UnstableInlineAccessorID // errorNumber: 192
209+
case ExtensionNullifiedByMemberID // errorNumber: 193
209210

210211
def errorNumber = ordinal - 1
211212

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,6 +2429,15 @@ class SynchronizedCallOnBoxedClass(stat: tpd.Tree)(using Context)
24292429
|you intended."""
24302430
}
24312431

2432+
class ExtensionNullifiedByMember(method: Symbol, target: Symbol)(using Context)
2433+
extends Message(ExtensionNullifiedByMemberID):
2434+
def kind = MessageKind.PotentialIssue
2435+
def msg(using Context) = i"Suspicious extension ${hl(method.name.toString)} is already a member of ${hl(target.name.toString)}"
2436+
def explain(using Context) =
2437+
i"""Extension method ${hl(method.name.toString)} will never be selected
2438+
|because ${hl(target.name.toString)} already has a member with the same name.
2439+
|It can be called as a regular method, but should probably be defined that way."""
2440+
24322441
class TraitCompanionWithMutableStatic()(using Context)
24332442
extends SyntaxMsg(TraitCompanionWithMutableStaticID) {
24342443
def msg(using Context) = i"Companion of traits cannot define mutable @static fields"

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

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

350366
trait Applications extends Compatibility {
@@ -1554,22 +1570,6 @@ trait Applications extends Compatibility {
15541570
tp
15551571
}
15561572

1557-
/** Drop any leading implicit parameter sections */
1558-
def stripImplicit(tp: Type, wildcardOnly: Boolean = false)(using Context): Type = tp match {
1559-
case mt: MethodType if mt.isImplicitMethod =>
1560-
stripImplicit(resultTypeApprox(mt, wildcardOnly))
1561-
case pt: PolyType =>
1562-
pt.derivedLambdaType(pt.paramNames, pt.paramInfos,
1563-
stripImplicit(pt.resultType, wildcardOnly = true))
1564-
// can't use TypeParamRefs for parameter references in `resultTypeApprox`
1565-
// since their bounds can refer to type parameters in `pt` that are not
1566-
// bound by the constraint. This can lead to hygiene violations if subsequently
1567-
// `pt` itself is added to the constraint. Test case is run/enrich-gentraversable.scala.
1568-
.asInstanceOf[PolyType].flatten
1569-
case _ =>
1570-
tp
1571-
}
1572-
15731573
/** Compare owner inheritance level.
15741574
* @param sym1 The first owner
15751575
* @param sym2 The second owner

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -985,8 +985,7 @@ object RefChecks {
985985
* surprising names at runtime. E.g. in neg/i4564a.scala, a private
986986
* case class `apply` method would have to be renamed to something else.
987987
*/
988-
def checkNoPrivateOverrides(tree: Tree)(using Context): Unit =
989-
val sym = tree.symbol
988+
def checkNoPrivateOverrides(sym: Symbol)(using Context): Unit =
990989
if sym.maybeOwner.isClass
991990
&& sym.is(Private)
992991
&& (sym.isOneOf(MethodOrLazyOrMutable) || !sym.is(Local)) // in these cases we'll produce a getter later
@@ -1048,6 +1047,28 @@ object RefChecks {
10481047

10491048
end checkUnaryMethods
10501049

1050+
/** Check that an extension method is not hidden, i.e., that it is callable.
1051+
*
1052+
* An extension method is hidden if it does not offer a parameter that is not subsumed
1053+
* by the corresponding parameter of the member (or of all alternatives of an overload).
1054+
*
1055+
* If the member has no parameters and the extension method has only implicit parameters,
1056+
* then warn that the extension is shadowed unless called with explicit arguments.
1057+
*/
1058+
def checkExtensionMethods(sym: Symbol)(using Context): Unit = if sym.is(Extension) then
1059+
extension (tp: Type) def firstExplicitParamTypes = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).firstParamTypes
1060+
val target = sym.info.firstParamTypes.head // required for extension method
1061+
if !target.typeSymbol.denot.isAliasType && !target.typeSymbol.denot.isOpaqueAlias then
1062+
val paramTps = sym.denot.info.resultType.firstExplicitParamTypes
1063+
val hidden =
1064+
target.nonPrivateMember(sym.name)
1065+
.filterWithPredicate:
1066+
_.info.firstExplicitParamTypes
1067+
.lazyZip(paramTps)
1068+
.forall((m, x) => x frozen_<:< m)
1069+
.exists
1070+
if hidden then report.warning(ExtensionNullifiedByMember(sym, target.typeSymbol), sym.srcPos)
1071+
10511072
/** Verify that references in the user-defined `@implicitNotFound` message are valid.
10521073
* (i.e. they refer to a type variable that really occurs in the signature of the annotated symbol.)
10531074
*/
@@ -1181,8 +1202,8 @@ class RefChecks extends MiniPhase { thisPhase =>
11811202

11821203
override def transformValDef(tree: ValDef)(using Context): ValDef = {
11831204
if tree.symbol.exists then
1184-
checkNoPrivateOverrides(tree)
11851205
val sym = tree.symbol
1206+
checkNoPrivateOverrides(sym)
11861207
if (sym.exists && sym.owner.isTerm) {
11871208
tree.rhs match {
11881209
case Ident(nme.WILDCARD) => report.error(UnboundPlaceholderParameter(), sym.srcPos)
@@ -1193,9 +1214,11 @@ class RefChecks extends MiniPhase { thisPhase =>
11931214
}
11941215

11951216
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
1196-
checkNoPrivateOverrides(tree)
1197-
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
1198-
checkUnaryMethods(tree.symbol)
1217+
val sym = tree.symbol
1218+
checkNoPrivateOverrides(sym)
1219+
checkImplicitNotFoundAnnotation.defDef(sym.denot)
1220+
checkUnaryMethods(sym)
1221+
checkExtensionMethods(sym)
11991222
tree
12001223
}
12011224

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2543,17 +2543,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25432543
vdef1.setDefTree
25442544
}
25452545

2546-
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = {
2547-
def canBeInvalidated(sym: Symbol): Boolean =
2546+
private def bailDefDef(sym: Symbol)(using Context): Tree =
2547+
// it's a discarded method (synthetic case class method or synthetic java record constructor), drop it
2548+
val canBeInvalidated: Boolean =
25482549
sym.is(Synthetic)
25492550
&& (desugar.isRetractableCaseClassMethodName(sym.name) ||
25502551
(sym.isConstructor && sym.owner.derivesFrom(defn.JavaRecordClass)))
2552+
assert(canBeInvalidated)
2553+
sym.owner.info.decls.openForMutations.unlink(sym)
2554+
EmptyTree
25512555

2552-
if !sym.info.exists then
2553-
// it's a discarded method (synthetic case class method or synthetic java record constructor), drop it
2554-
assert(canBeInvalidated(sym))
2555-
sym.owner.info.decls.openForMutations.unlink(sym)
2556-
return EmptyTree
2556+
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if !sym.info.exists then bailDefDef(sym) else {
25572557

25582558
// TODO: - Remove this when `scala.language.experimental.erasedDefinitions` is no longer experimental.
25592559
// - Modify signature to `erased def erasedValue[T]: T`
@@ -2576,7 +2576,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25762576
case untpd.TypeDefs(tparams) => tparams
25772577
}
25782578

2579-
// Register GADT constraint for class type parameters from outer to inner class definition. (Useful when nested classes exist.) But do not cross a function definition.
2579+
// Register GADT constraint for class type parameters from outer to inner class definition.
2580+
// (Useful when nested classes exist.) But do not cross a function definition.
25802581
if sym.flags.is(Method) then
25812582
rhsCtx.setFreshGADTBounds
25822583
ctx.outer.outersIterator.takeWhile(!_.owner.is(Method))

compiler/test/dotty/tools/dotc/printing/PrintingTest.scala

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class B {
4545
class C {
4646
def extensionInCompanion: String = ???
4747
}
48-
48+
@annotation.nowarn // extensionInCompanion
4949
object C {
5050
implicit def companionConversion(c: C): B = ???
5151

@@ -70,4 +70,4 @@ package nested {
7070
}
7171

7272
class Z
73-
}
73+
}

scaladoc-testcases/src/tests/inheritedMembers1.scala

Lines changed: 1 addition & 0 deletions
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/neg/i16743.check

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:26:6 -----------------------------------------------------------
2+
26 | def t = 27 // error
3+
| ^
4+
| Suspicious extension t is already a member of T
5+
|
6+
| longer explanation available when compiling with `-explain`
7+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:28:6 -----------------------------------------------------------
8+
28 | def g(x: String)(i: Int): String = x*i // error
9+
| ^
10+
| Suspicious extension g is already a member of T
11+
|
12+
| longer explanation available when compiling with `-explain`
13+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:29:6 -----------------------------------------------------------
14+
29 | def h(x: String): String = x // error
15+
| ^
16+
| Suspicious extension h is already a member of T
17+
|
18+
| longer explanation available when compiling with `-explain`
19+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:31:6 -----------------------------------------------------------
20+
31 | def j(x: Any, y: Int): String = (x.toString)*y // error
21+
| ^
22+
| Suspicious extension j is already a member of T
23+
|
24+
| longer explanation available when compiling with `-explain`
25+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:32:6 -----------------------------------------------------------
26+
32 | def k(x: String): String = x // error
27+
| ^
28+
| Suspicious extension k is already a member of T
29+
|
30+
| longer explanation available when compiling with `-explain`
31+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:33:6 -----------------------------------------------------------
32+
33 | def l(using String): String = summon[String] // error: can't be called implicitly
33+
| ^
34+
| Suspicious extension l is already a member of T
35+
|
36+
| longer explanation available when compiling with `-explain`
37+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:34:6 -----------------------------------------------------------
38+
34 | def m(using String): String = "m" + summon[String] // error
39+
| ^
40+
| Suspicious extension m is already a member of T
41+
|
42+
| longer explanation available when compiling with `-explain`
43+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:35:6 -----------------------------------------------------------
44+
35 | def n(using String): String = "n" + summon[String] // error
45+
| ^
46+
| Suspicious extension n is already a member of T
47+
|
48+
| longer explanation available when compiling with `-explain`
49+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:36:6 -----------------------------------------------------------
50+
36 | def o: String = "42" // error
51+
| ^
52+
| Suspicious extension o is already a member of T
53+
|
54+
| longer explanation available when compiling with `-explain`
55+
-- [E193] Potential Issue Error: tests/neg/i16743.scala:37:6 -----------------------------------------------------------
56+
37 | def u: Int = 27 // error
57+
| ^
58+
| Suspicious extension u is already a member of T
59+
|
60+
| longer explanation available when compiling with `-explain`

tests/neg/i16743.scala

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
//> using options -Werror
3+
4+
trait G
5+
given G = new G { override def toString = "mygiven" }
6+
given String = "aGivenString"
7+
8+
trait T:
9+
def t = 42
10+
def f(x: String): String = x*2
11+
def g(x: String)(y: String): String = (x+y)*2
12+
def h(x: Any): String = x.toString*2
13+
def i(x: Any, y: String): String = (x.toString+y)*2
14+
def j(x: Any, y: Any): String = (x.toString+y.toString)
15+
def k(using G): String = summon[G].toString
16+
def l(using G): String = summon[G].toString
17+
def m: String = "mystring"
18+
def n: Result = Result()
19+
def o: Int = 42
20+
def u: Int = 42
21+
def u(n: Int): Int = u + n
22+
def v(n: Int): Int = u + n
23+
def v(s: String): String = s + u
24+
25+
extension (_t: T)
26+
def t = 27 // error
27+
def f(i: Int): String = String.valueOf(i)
28+
def g(x: String)(i: Int): String = x*i // error
29+
def h(x: String): String = x // error
30+
def i(x: Any, y: Int): String = (x.toString)*y
31+
def j(x: Any, y: Int): String = (x.toString)*y // error
32+
def k(x: String): String = x // error
33+
def l(using String): String = summon[String] // error: can't be called implicitly
34+
def m(using String): String = "m" + summon[String] // error
35+
def n(using String): String = "n" + summon[String] // error
36+
def o: String = "42" // error
37+
def u: Int = 27 // error
38+
def v(d: Double) = 3.14
39+
40+
// deferred extension is defined in subclass
41+
trait Foo:
42+
type X
43+
extension (x: X) def t: Int
44+
45+
trait Bar extends Foo:
46+
type X = T
47+
extension (x: X) def t = x.t
48+
49+
// extension on opaque type matches member of underlying type
50+
opaque type IArray[+T] = Array[? <: T]
51+
object IArray:
52+
extension (arr: IArray[Byte]) def length: Int = arr.asInstanceOf[Array[Byte]].length
53+
54+
class Result:
55+
def apply(using String): String = s"result ${summon[String]}"
56+
57+
@main def test() =
58+
val x = new T {}
59+
println(x.f(42)) // OK!
60+
//println(x.g("x")(42)) // NOT OK!
61+
println(x.h("hi")) // member!
62+
println(x.i("hi", 5)) // OK!
63+
println(x.j("hi", 5)) // member!
64+
println(x.k)
65+
println(x.l(using "x"))
66+
println(x.l)
67+
println(x.m(using "x"))
68+
println(x.m(2))
69+
println(x.n) // OK just checks
70+
println(x.n(using "x")) // also just checks

tests/warn/i9241.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ final class Baz private (val x: Int) extends AnyVal {
2222
}
2323

2424
extension (x: Int)
25+
@annotation.nowarn
2526
def unary_- : Int = ???
2627
def unary_+[T] : Int = ???
2728
def unary_!() : Int = ??? // warn

0 commit comments

Comments
 (0)