Skip to content

Commit ae2d781

Browse files
committed
Fix #4564: Invalidate clashing case class methods
Invalidate a compiler generated case class method that clashes with a user-defined method in the same scope with a matching type. Invalidation is done by overwriting its info on completion with `NoType` and eliminating the method altogether during type checking. Methods potentially affected are `apply`, `unapply`, selectors `_1`, `_2`, ... `copy`, and `copy`'s default getters.
1 parent 0db2b36 commit ae2d781

File tree

5 files changed

+113
-6
lines changed

5 files changed

+113
-6
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ object desugar {
2121
private type VarInfo = (NameTree, Tree)
2222

2323
/** Names of methods that are added unconditionally to case classes */
24-
def isDesugaredCaseClassMethodName(name: Name)(implicit ctx: Context): Boolean =
25-
name == nme.copy || name.isSelectorName
24+
def isDesugaredCaseClassMethodName(name: Name)(implicit ctx: Context): Boolean = name match {
25+
case nme.apply | nme.unapply | nme.copy => true
26+
case DefaultGetterName(nme.copy, _) => true
27+
case _ => name.isSelectorName
28+
}
2629

2730
// ----- DerivedTypeTrees -----------------------------------
2831

@@ -207,8 +210,7 @@ object desugar {
207210
tpt = TypeTree(),
208211
rhs = vparam.rhs
209212
)
210-
.withMods(Modifiers(mods.flags & AccessFlags, mods.privateWithin))
211-
.withFlags(Synthetic)
213+
.withMods(Modifiers(mods.flags & (AccessFlags | Synthetic), mods.privateWithin))
212214
val rest = defaultGetters(vparams :: vparamss1, n + 1)
213215
if (vparam.rhs.isEmpty) rest else defaultGetter :: rest
214216
case Nil :: vparamss1 =>

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,28 @@ class Namer { typer: Typer =>
768768
case _ =>
769769
}
770770

771+
/** Invalidate `denot` by overwriting its info with `NoType` if
772+
* `denot` is a compiler generated case class method that clashes
773+
* with a user-defined method in the same scope with a matching type.
774+
*/
775+
private def invalidateIfClashingSynthetic(denot: SymDenotation): Unit = {
776+
def isCaseClass(owner: Symbol) =
777+
owner.isClass && {
778+
if (owner.is(Module)) owner.linkedClass.is(CaseClass)
779+
else owner.is(CaseClass)
780+
}
781+
val isClashingSynthetic =
782+
denot.is(Synthetic) &&
783+
desugar.isDesugaredCaseClassMethodName(denot.name) &&
784+
isCaseClass(denot.owner) &&
785+
denot.owner.info.decls.lookupAll(denot.name).exists(alt =>
786+
alt != denot.symbol && alt.info.matchesLoosely(denot.info))
787+
if (isClashingSynthetic) {
788+
typr.println(i"invalidating clashing $denot in ${denot.owner}")
789+
denot.info = NoType
790+
}
791+
}
792+
771793
/** Intentionally left without `implicit ctx` parameter. We need
772794
* to pick up the context at the point where the completer was created.
773795
*/
@@ -776,6 +798,7 @@ class Namer { typer: Typer =>
776798
addAnnotations(sym)
777799
addInlineInfo(sym)
778800
denot.info = typeSig(sym)
801+
invalidateIfClashingSynthetic(denot)
779802
Checking.checkWellFormed(sym)
780803
denot.info = avoidPrivateLeaks(sym, sym.pos)
781804
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,7 +1413,12 @@ class Typer extends Namer
14131413
}
14141414
}
14151415

1416-
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(implicit ctx: Context) = track("typedDefDef") {
1416+
def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(implicit ctx: Context): Tree = track("typedDefDef") {
1417+
if (!sym.info.exists) { // it's a discarded synthetic case class method, drop it
1418+
assert(sym.is(Synthetic) && desugar.isDesugaredCaseClassMethodName(sym.name))
1419+
sym.owner.info.decls.openForMutations.unlink(sym)
1420+
return EmptyTree
1421+
}
14171422
val DefDef(name, tparams, vparamss, tpt, _) = ddef
14181423
completeAnnotations(ddef, sym)
14191424
val tparams1 = tparams mapconserve (typed(_).asInstanceOf[TypeDef])
@@ -1912,7 +1917,8 @@ class Typer extends Namer
19121917
enumContexts(mdef1.symbol) = ctx
19131918
case _ =>
19141919
}
1915-
buf += mdef1
1920+
if (!mdef1.isEmpty) // clashing synthetic case methods are converted to empty trees
1921+
buf += mdef1
19161922
}
19171923
traverse(rest)
19181924
}

tests/pos/i4564.scala

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
case class A(x: Int) {
2+
def copy(x: Int = x) = A(x)
3+
}
4+
5+
// NOTE: the companion inherits a public apply method from Function1!
6+
case class NeedsCompanion private (x: Int)
7+
8+
object ClashNoSig { // ok
9+
private def apply(x: Int) = if (x > 0) new ClashNoSig(x) else ???
10+
def unapply(x: ClashNoSig) = x
11+
12+
ClashNoSig(2) match {
13+
case c @ ClashNoSig(y) => c.copy(y + c._1)
14+
}
15+
}
16+
case class ClashNoSig private (x: Int) {
17+
def _1: Int = x
18+
def copy(y: Int) = ClashNoSig(y)
19+
}
20+
21+
object Clash {
22+
private def apply(x: Int) = if (x > 0) new Clash(x) else ???
23+
}
24+
case class Clash private (x: Int)
25+
26+
object ClashSig {
27+
private def apply(x: Int): ClashSig = if (x > 0) new ClashSig(x) else ???
28+
}
29+
case class ClashSig private (x: Int)
30+
31+
object ClashOverload {
32+
private def apply(x: Int): ClashOverload = if (x > 0) new ClashOverload(x) else apply("")
33+
def apply(x: String): ClashOverload = ???
34+
}
35+
case class ClashOverload private (x: Int)
36+
37+
object NoClashSig {
38+
private def apply(x: Boolean): NoClashSig = if (x) NoClashSig(1) else ???
39+
}
40+
case class NoClashSig private (x: Int)
41+
42+
object NoClashOverload {
43+
// needs full sig
44+
private def apply(x: Boolean): NoClashOverload = if (x) NoClashOverload(1) else apply("")
45+
def apply(x: String): NoClashOverload = ???
46+
}
47+
case class NoClashOverload private (x: Int)
48+
49+
class BaseNCP[T] {
50+
// error: overloaded method apply needs result type
51+
def apply(x: T): NoClashPoly = if (???) NoClashPoly(1) else ???
52+
}
53+
54+
object NoClashPoly extends BaseNCP[Boolean]
55+
case class NoClashPoly private(x: Int)
56+
57+
58+
class BaseCP[T] {
59+
// error: overloaded method apply needs result type
60+
def apply(x: T): ClashPoly = if (???) ClashPoly(1) else ???
61+
}
62+
object ClashPoly extends BaseCP[Int]
63+
case class ClashPoly private(x: Int)

tests/pos/t5816-noclash.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
object Foo {
2+
// spurious error if:
3+
// - this definition precedes that of apply (which is overloaded with the synthetic one derived from the case class)
4+
// - AND `Foo.apply` is explicitly applied to `[A]` (no error if `[A]` is inferred)
5+
//
6+
def referToPolyOverloadedApply[A]: Foo[A] = Foo.apply[A]("bla")
7+
// ^
8+
// found : String("bla")
9+
// required: Int
10+
11+
def apply[A](x: Int): Foo[A] = ???
12+
}
13+
case class Foo[A](x: String)

0 commit comments

Comments
 (0)