Skip to content

Commit 4afa0c3

Browse files
authored
Merge pull request #4813 from dotty-staging/fix-#4564
Fix #4564: Invalidate clashing case class methods
2 parents 859c5db + c26f319 commit 4afa0c3

File tree

6 files changed

+180
-10
lines changed

6 files changed

+180
-10
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ object desugar {
2020
/** Info of a variable in a pattern: The named tree and its type */
2121
private type VarInfo = (NameTree, Tree)
2222

23-
/** Names of methods that are added unconditionally to case classes */
23+
/** Is `name` the name of a method that can be invalidated as a compiler-generated
24+
* case class method that clashes with a user-defined method?
25+
*/
26+
def isRetractableCaseClassMethodName(name: Name)(implicit ctx: Context): Boolean = name match {
27+
case nme.apply | nme.unapply | nme.copy => true
28+
case DefaultGetterName(nme.copy, _) => true
29+
case _ => false
30+
}
31+
32+
/** Is `name` the name of a method that is added unconditionally to case classes? */
2433
def isDesugaredCaseClassMethodName(name: Name)(implicit ctx: Context): Boolean =
25-
name == nme.copy || name.isSelectorName
34+
isRetractableCaseClassMethodName(name) || name.isSelectorName
2635

2736
// ----- DerivedTypeTrees -----------------------------------
2837

@@ -207,8 +216,7 @@ object desugar {
207216
tpt = TypeTree(),
208217
rhs = vparam.rhs
209218
)
210-
.withMods(Modifiers(mods.flags & AccessFlags, mods.privateWithin))
211-
.withFlags(Synthetic)
219+
.withMods(Modifiers(mods.flags & (AccessFlags | Synthetic), mods.privateWithin))
212220
val rest = defaultGetters(vparams :: vparamss1, n + 1)
213221
if (vparam.rhs.isEmpty) rest else defaultGetter :: rest
214222
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.isRetractableCaseClassMethodName(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: 24 additions & 6 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.isRetractableCaseClassMethodName(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])
@@ -1911,7 +1916,8 @@ class Typer extends Namer
19111916
enumContexts(mdef1.symbol) = ctx
19121917
case _ =>
19131918
}
1914-
buf += mdef1
1919+
if (!mdef1.isEmpty) // clashing synthetic case methods are converted to empty trees
1920+
buf += mdef1
19151921
}
19161922
traverse(rest)
19171923
}
@@ -1986,7 +1992,6 @@ class Typer extends Namer
19861992
* (but do this at most once per tree).
19871993
*
19881994
* After that, two strategies are tried, and the first that is successful is picked.
1989-
* If neither of the strategies are successful, continues with`fallBack`.
19901995
*
19911996
* 1st strategy: Try to insert `.apply` so that the result conforms to prototype `pt`.
19921997
* This strategy is not tried if the prototype represents already
@@ -1995,6 +2000,10 @@ class Typer extends Namer
19952000
* 2nd strategy: If tree is a select `qual.name`, try to insert an implicit conversion
19962001
* around the qualifier part `qual` so that the result conforms to the expected type
19972002
* with wildcard result type.
2003+
*
2004+
* If neither of the strategies are successful, continues with the `apply` result
2005+
* if an apply insertion was tried and `tree` has an `apply` method, or continues
2006+
* with `fallBack` otherwise. `fallBack` is supposed to always give an error.
19982007
*/
19992008
def tryInsertApplyOrImplicit(tree: Tree, pt: ProtoType, locked: TypeVars)(fallBack: => Tree)(implicit ctx: Context): Tree = {
20002009

@@ -2016,7 +2025,7 @@ class Typer extends Namer
20162025
else try adapt(simplify(sel, pt, locked), pt, locked) finally sel.removeAttachment(InsertedApply)
20172026
}
20182027

2019-
def tryImplicit =
2028+
def tryImplicit(fallBack: => Tree) =
20202029
tryInsertImplicitOnQualifier(tree, pt, locked).getOrElse(fallBack)
20212030

20222031
pt match {
@@ -2027,8 +2036,17 @@ class Typer extends Namer
20272036
pt.markAsDropped()
20282037
tree
20292038
case _ =>
2030-
if (isApplyProto(pt) || isMethod(tree) || isSyntheticApply(tree)) tryImplicit
2031-
else tryEither(tryApply(_))((_, _) => tryImplicit)
2039+
if (isApplyProto(pt) || isMethod(tree) || isSyntheticApply(tree)) tryImplicit(fallBack)
2040+
else tryEither(tryApply(_)) { (app, appState) =>
2041+
tryImplicit {
2042+
if (tree.tpe.member(nme.apply).exists) {
2043+
// issue the error about the apply, since it is likely more informative than the fallback
2044+
appState.commit()
2045+
app
2046+
}
2047+
else fallBack
2048+
}
2049+
}
20322050
}
20332051
}
20342052

tests/neg/i4564.scala

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
object ClashOverloadNoSig {
2+
3+
private def apply(x: Int) = if (x > 0) new ClashOverloadNoSig(x) else apply("") // error: overloaded method apply needs result type
4+
5+
def apply(x: String): ClashOverloadNoSig = ???
6+
}
7+
8+
case class ClashOverloadNoSig private(x: Int)
9+
10+
object ClashRecNoSig {
11+
private def apply(x: Int) = if (x > 0) ClashRecNoSig(1) else ??? // error: recursive method apply needs result type
12+
}
13+
14+
case class ClashRecNoSig private(x: Int)
15+
16+
object NoClashNoSig {
17+
private def apply(x: Boolean) = if (x) NoClashNoSig(1) else ??? // error: overloaded method apply needs result type
18+
}
19+
20+
case class NoClashNoSig private(x: Int)
21+
22+
object NoClashOverload {
23+
private def apply(x: Boolean) = if (x) NoClashOverload(1) else apply("") // error // error: overloaded method apply needs result type (twice)
24+
25+
def apply(x: String): NoClashOverload = ???
26+
}
27+
28+
case class NoClashOverload private(x: Int)
29+
30+
31+
class BaseNCNSP[T] {
32+
def apply(x: T) = if (???) NoClashNoSigPoly(1) else ??? // error: overloaded method apply needs result type
33+
}
34+
35+
object NoClashNoSigPoly extends BaseNCNSP[Boolean]
36+
case class NoClashNoSigPoly private(x: Int) // ok, since `apply` is in base class
37+
38+
39+
40+
class BaseCNSP[T] {
41+
def apply(x: T) = if (???) ClashNoSigPoly(1) else ??? // error: recursive method apply needs result type
42+
}
43+
44+
object ClashNoSigPoly extends BaseCNSP[Int]
45+
// TODO: improve error message
46+
case class ClashNoSigPoly private(x: Int) // error: found: ClashNoSigPoly required: Nothing

tests/pos/i4564.scala

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 copy(y: Int) = ClashNoSig(y)
18+
}
19+
20+
object Clash {
21+
private def apply(x: Int) = if (x > 0) new Clash(x) else ???
22+
}
23+
case class Clash private (x: Int)
24+
25+
object ClashSig {
26+
private def apply(x: Int): ClashSig = if (x > 0) new ClashSig(x) else ???
27+
}
28+
case class ClashSig private (x: Int)
29+
30+
object ClashOverload {
31+
private def apply(x: Int): ClashOverload = if (x > 0) new ClashOverload(x) else apply("")
32+
def apply(x: String): ClashOverload = ???
33+
}
34+
case class ClashOverload private (x: Int)
35+
36+
object NoClashSig {
37+
private def apply(x: Boolean): NoClashSig = if (x) NoClashSig(1) else ???
38+
}
39+
case class NoClashSig private (x: Int)
40+
41+
object NoClashOverload {
42+
// needs full sig
43+
private def apply(x: Boolean): NoClashOverload = if (x) NoClashOverload(1) else apply("")
44+
def apply(x: String): NoClashOverload = ???
45+
}
46+
case class NoClashOverload private (x: Int)
47+
48+
class BaseNCP[T] {
49+
// error: overloaded method apply needs result type
50+
def apply(x: T): NoClashPoly = if (???) NoClashPoly(1) else ???
51+
}
52+
53+
object NoClashPoly extends BaseNCP[Boolean]
54+
case class NoClashPoly private(x: Int)
55+
56+
57+
class BaseCP[T] {
58+
// error: overloaded method apply needs result type
59+
def apply(x: T): ClashPoly = if (???) ClashPoly(1) else ???
60+
}
61+
object ClashPoly extends BaseCP[Int]
62+
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)